diff --git a/.editorconfig b/.editorconfig index f2899c92af..c8b73b2d84 100644 --- a/.editorconfig +++ b/.editorconfig @@ -16,8 +16,154 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false csharp_space_between_method_call_parameter_list_parentheses = false csharp_preserve_single_line_blocks = true dotnet_style_require_accessibility_modifiers = never -csharp_style_var_when_type_is_apparent = true -csharp_prefer_braces = false +csharp_style_var_when_type_is_apparent = true:none +csharp_prefer_braces = true:none csharp_space_before_open_square_brackets = true csharp_space_between_method_call_name_and_opening_parenthesis = true csharp_space_between_method_declaration_name_and_open_parenthesis = true + +# Microsoft .NET properties +csharp_style_var_elsewhere = true:none + +# ReSharper properties +resharper_align_linq_query = true +resharper_align_multiline_calls_chain = true +resharper_align_multiline_extends_list = true +resharper_align_multiline_parameter = true +resharper_blank_lines_around_region = 1 +resharper_braces_redundant = true +resharper_csharp_stick_comment = false +resharper_force_attribute_style = separate +resharper_indent_type_constraints = true +resharper_local_function_body = expression_body +resharper_remove_blank_lines_near_braces_in_declarations = true +resharper_use_roslyn_logic_for_evident_types = true +csharp_space_around_binary_operators = before_and_after +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:none +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_primary_constructors = true:suggestion +csharp_style_expression_bodied_methods = true:none +csharp_style_expression_bodied_constructors = true:none +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_style_throw_expression = true:suggestion +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_tuple_swap = true:suggestion +csharp_style_prefer_utf8_string_literals = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_style_unused_value_assignment_preference = discard_variable:suggestion +csharp_style_unused_value_expression_statement_preference = discard_variable:silent +csharp_indent_case_contents_when_block = true +csharp_prefer_static_local_function = true:suggestion +csharp_style_prefer_readonly_struct = true:suggestion +csharp_style_prefer_readonly_struct_member = true:suggestion +csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent +csharp_style_allow_blank_lines_between_consecutive_braces_experimental = true:silent +csharp_style_allow_blank_line_after_colon_in_constructor_initializer_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_conditional_expression_experimental = true:silent +csharp_style_allow_blank_line_after_token_in_arrow_expression_clause_experimental = true:silent +csharp_style_conditional_delegate_call = true:suggestion +csharp_style_prefer_switch_expression = true:suggestion +csharp_style_prefer_pattern_matching = true:silent +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_prefer_not_pattern = true:suggestion +csharp_style_prefer_extended_property_pattern = true:suggestion +csharp_style_var_for_built_in_types = false:silent + +[*.{cs,vb}] +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 8 +indent_size = 8 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_readonly_field = true:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +dotnet_style_require_accessibility_modifiers = never:silent +dotnet_style_allow_multiple_blank_lines_experimental = true:silent +dotnet_style_allow_statement_immediately_after_block_experimental = true:silent +dotnet_code_quality_unused_parameters = all:suggestion +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index 4e91d239fb..ed4eca8cb4 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -19,7 +19,7 @@ jobs: runs-on: windows-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: DocFX Build working-directory: docfx diff --git a/.github/workflows/dotnet-core.yml b/.github/workflows/dotnet-core.yml index 39bc0906e4..f1afbb057a 100644 --- a/.github/workflows/dotnet-core.yml +++ b/.github/workflows/dotnet-core.yml @@ -9,15 +9,15 @@ on: jobs: build_and_test: - runs-on: ubuntu-latest + runs-on: windows-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0 + dotnet-version: 8.0 dotnet-quality: 'ga' - name: Install dependencies @@ -30,7 +30,7 @@ jobs: - name: Test run: | sed -i 's/"stopOnFail": false/"stopOnFail": true/g' UnitTests/xunit.runner.json - dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --settings UnitTests/coverlet.runsettings + dotnet test --no-restore --verbosity normal --collect:"XPlat Code Coverage" --settings UnitTests/coverlet.runsettings --blame mv -v UnitTests/TestResults/*/*.* UnitTests/TestResults/ # Note: this step is currently not writing to the gist for some reason diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index db34f23bbd..6eda0c8c3a 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 # fetch-depth is needed for GitVersion @@ -34,7 +34,7 @@ jobs: - name: Setup dotnet uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0 + dotnet-version: 8.0 dotnet-quality: 'ga' - name: Install dependencies diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3c7851b858..32f409dbd6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -156,7 +156,7 @@ The [Microsoft .NET Framework Design Guidelines](https://docs.microsoft.com/en-u - Sub-classes of the class implementing `EventToRaise` can override `OnEventToRaise` as needed. 4. Where possible, a subclass of `EventArgs` should be provided and the old and new state should be included. By doing this, event handler methods do not have to query the sender for state. -See also: https://www.codeproject.com/docs/20550/C-Event-Implementation-Fundamentals-Best-Practices +See also: https://www.codeproject.com../docs/20550/C-Event-Implementation-Fundamentals-Best-Practices ### Defining new `View` classes diff --git a/Example/Example.csproj b/Example/Example.csproj index 845bf6d6a2..4bb8cc0bc0 100644 --- a/Example/Example.csproj +++ b/Example/Example.csproj @@ -1,7 +1,7 @@  Exe - net7.0 + net8.0 diff --git a/ReactiveExample/LoginViewModel.cs b/ReactiveExample/LoginViewModel.cs index 619e4c8022..edd7b63dd2 100644 --- a/ReactiveExample/LoginViewModel.cs +++ b/ReactiveExample/LoginViewModel.cs @@ -17,7 +17,7 @@ namespace ReactiveExample { // We mark the view model with the [DataContract] attributes and this // allows you to save the view model class to the disk, and then to read // the view model from the disk, making your app state persistent. - // See also: https://www.reactiveui.net/docs/handbook/data-persistence/ + // See also: https://www.reactiveui.net../docs/handbook/data-persistence/ // [DataContract] public class LoginViewModel : ReactiveObject { diff --git a/ReactiveExample/README.md b/ReactiveExample/README.md index 61f86cfa5d..f5dbf705b3 100644 --- a/ReactiveExample/README.md +++ b/ReactiveExample/README.md @@ -17,7 +17,7 @@ From now on, you can use `.ObserveOn(RxApp.MainThreadScheduler)` to return to th ### Data Bindings -If you wish to implement `OneWay` data binding, then use the `WhenAnyValue` [ReactiveUI extension method](https://www.reactiveui.net/docs/handbook/when-any/) that listens to `INotifyPropertyChanged` events of the specified property, and converts that events into `IObservable`: +If you wish to implement `OneWay` data binding, then use the `WhenAnyValue` [ReactiveUI extension method](https://www.reactiveui.net../docs/handbook/when-any/) that listens to `INotifyPropertyChanged` events of the specified property, and converts that events into `IObservable`: ```cs // 'usernameInput' is 'TextField' diff --git a/ReactiveExample/ReactiveExample.csproj b/ReactiveExample/ReactiveExample.csproj index a9269f8bfd..b3c0c5558e 100644 --- a/ReactiveExample/ReactiveExample.csproj +++ b/ReactiveExample/ReactiveExample.csproj @@ -1,7 +1,7 @@  Exe - net7.0 + net8.0 @@ -11,8 +11,8 @@ 2.0 - - + + diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application.cs index 368552192d..c57b6619cc 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; namespace Terminal.Gui; + /// /// A static, singleton class representing the application. This class is the entry point for the application. /// @@ -28,20 +29,9 @@ namespace Terminal.Gui; /// /// /// -/// -/// Creates a instance of to process input events, handle timers and -/// other sources of data. It is accessible via the property. -/// -/// -/// The event is invoked on each iteration of the . -/// -/// -/// When invoked it sets the to one that is tied -/// to the , allowing user code to use async/await. -/// +/// TODO: Flush this out. /// public static partial class Application { - /// /// Gets the that has been selected. See also . /// @@ -64,19 +54,19 @@ public static partial class Application { // For Unit testing - ignores UseSystemConsole internal static bool _forceFakeConsole; - private static List _cachedSupportedCultures; + static List _cachedSupportedCultures; /// /// Gets all cultures supported by the application without the invariant language. /// public static List SupportedCultures => _cachedSupportedCultures; - private static List GetSupportedCultures () + static List GetSupportedCultures () { - CultureInfo [] culture = CultureInfo.GetCultures (CultureTypes.AllCultures); + var culture = CultureInfo.GetCultures (CultureTypes.AllCultures); // Get the assembly - Assembly assembly = Assembly.GetExecutingAssembly (); + var assembly = Assembly.GetExecutingAssembly (); //Find the location of the assembly string assemblyLocation = AppDomain.CurrentDomain.BaseDirectory; @@ -86,13 +76,12 @@ private static List GetSupportedCultures () // Return all culture for which satellite folder found with culture code. return culture.Where (cultureInfo => - Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) && - File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) + Directory.Exists (Path.Combine (assemblyLocation, cultureInfo.Name)) && + File.Exists (Path.Combine (assemblyLocation, cultureInfo.Name, resourceFilename)) ).ToList (); } #region Initialization (Init/Shutdown) - /// /// Initializes a new instance of Application. /// @@ -155,17 +144,19 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver // multiple times. We need to do this because some settings are only // valid after a Driver is loaded. In this cases we need just // `Settings` so we can determine which driver to use. - ConfigurationManager.Load (true); - ConfigurationManager.Apply (); + Load (true); + Apply (); Driver ??= Environment.OSVersion.Platform switch { _ when _forceFakeConsole => new FakeDriver (), // for unit testing only _ when UseSystemConsole => new NetDriver (), PlatformID.Win32NT or PlatformID.Win32S or PlatformID.Win32Windows => new WindowsDriver (), - _ => new CursesDriver (), + _ => new CursesDriver () }; - if (Driver == null) throw new InvalidOperationException ("Init could not determine the ConsoleDriver to use."); + if (Driver == null) { + throw new InvalidOperationException ("Init could not determine the ConsoleDriver to use."); + } try { MainLoop = Driver.Init (); @@ -177,11 +168,10 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver throw new InvalidOperationException ("Unable to initialize the console. This can happen if the console is already in use by another process or in unit tests.", ex); } - Driver.SizeChanged += (s, args) => OnSizeChanging (args); - Driver.KeyPressed += (s, args) => OnKeyPressed (args); - Driver.KeyDown += (s, args) => OnKeyDown (args); - Driver.KeyUp += (s, args) => OnKeyUp (args); - Driver.MouseEvent += (s, args) => OnMouseEvent (args); + Driver.SizeChanged += Driver_SizeChanged; + Driver.KeyDown += Driver_KeyDown; + Driver.KeyUp += Driver_KeyUp; + Driver.MouseEvent += Driver_MouseEvent; SynchronizationContext.SetSynchronizationContext (new MainLoopSyncContext ()); @@ -192,6 +182,14 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver _initialized = true; } + static void Driver_SizeChanged (object sender, SizeChangedEventArgs e) => OnSizeChanging (e); + + static void Driver_KeyDown (object sender, Key e) => OnKeyDown (e); + + static void Driver_KeyUp (object sender, Key e) => OnKeyUp (e); + + static void Driver_MouseEvent (object sender, MouseEventEventArgs e) => OnMouseEvent (e); + /// /// Shutdown an application initialized with . @@ -203,7 +201,7 @@ internal static void InternalInit (Func topLevelFactory, ConsoleDriver public static void Shutdown () { ResetState (); - ConfigurationManager.PrintJsonErrors (); + PrintJsonErrors (); } // Encapsulate all setting of initial state for Application; Having @@ -228,13 +226,18 @@ static void ResetState () MainLoop?.Dispose (); MainLoop = null; - Driver?.End (); - Driver = null; + if (Driver != null) { + Driver.SizeChanged -= Driver_SizeChanged; + Driver.KeyDown -= Driver_KeyDown; + Driver.KeyUp -= Driver_KeyUp; + Driver.MouseEvent -= Driver_MouseEvent; + Driver?.End (); + Driver = null; + } Iteration = null; MouseEvent = null; KeyDown = null; KeyUp = null; - KeyPressed = null; SizeChanging = null; _mainThreadId = -1; NotifyNewRunState = null; @@ -249,11 +252,9 @@ static void ResetState () // (https://github.com/gui-cs/Terminal.Gui/issues/1084). SynchronizationContext.SetSynchronizationContext (syncContext: null); } - #endregion Initialization (Init/Shutdown) #region Run (Begin, Run, End, Stop) - /// /// Notify that a new was created ( was called). The token is created in /// and this event will be fired before that function exits. @@ -317,8 +318,8 @@ public static RunState Begin (Toplevel Toplevel) Top.OnLeave (Toplevel); } if (string.IsNullOrEmpty (Toplevel.Id)) { - var count = 1; - var id = (_topLevels.Count + count).ToString (); + int count = 1; + string id = (_topLevels.Count + count).ToString (); while (_topLevels.Count > 0 && _topLevels.FirstOrDefault (x => x.Id == id) != null) { count++; id = (_topLevels.Count + count).ToString (); @@ -341,9 +342,9 @@ public static RunState Begin (Toplevel Toplevel) Top = Toplevel; } - var refreshDriver = true; - if (OverlappedTop == null || Toplevel.IsOverlappedContainer || (Current?.Modal == false && Toplevel.Modal) - || (Current?.Modal == false && !Toplevel.Modal) || (Current?.Modal == true && Toplevel.Modal)) { + bool refreshDriver = true; + if (OverlappedTop == null || Toplevel.IsOverlappedContainer || Current?.Modal == false && Toplevel.Modal + || Current?.Modal == false && !Toplevel.Modal || Current?.Modal == true && Toplevel.Modal) { if (Toplevel.Visible) { Current = Toplevel; @@ -351,8 +352,8 @@ public static RunState Begin (Toplevel Toplevel) } else { refreshDriver = false; } - } else if ((OverlappedTop != null && Toplevel != OverlappedTop && Current?.Modal == true && !_topLevels.Peek ().Modal) - || (OverlappedTop != null && Toplevel != OverlappedTop && Current?.Running == false)) { + } else if (OverlappedTop != null && Toplevel != OverlappedTop && Current?.Modal == true && !_topLevels.Peek ().Modal + || OverlappedTop != null && Toplevel != OverlappedTop && Current?.Running == false) { refreshDriver = false; MoveCurrent (Toplevel); } else { @@ -406,7 +407,7 @@ public static RunState Begin (Toplevel Toplevel) /// platform will be used (, , or ). /// Must be if has already been called. /// - public static void Run (Func errorHandler = null, ConsoleDriver driver = null) where T : Toplevel, new() + public static void Run (Func errorHandler = null, ConsoleDriver driver = null) where T : Toplevel, new () { if (_initialized) { if (Driver != null) { @@ -426,7 +427,7 @@ public static RunState Begin (Toplevel Toplevel) } } else { // Init() has NOT been called. - InternalInit (() => new T (), driver, calledViaRunT: true); + InternalInit (() => new T (), driver, true); Run (Top, errorHandler); } } @@ -465,7 +466,7 @@ public static RunState Begin (Toplevel Toplevel) /// RELEASE builds only: Handler for any unhandled exceptions (resumes when returns true, rethrows when null). public static void Run (Toplevel view, Func errorHandler = null) { - var resume = true; + bool resume = true; while (resume) { #if !DEBUG try { @@ -520,13 +521,10 @@ public static void Run (Toplevel view, Func errorHandler = null /// Runs on the thread that is processing events /// /// the action to be invoked on the main processing thread. - public static void Invoke (Action action) - { - MainLoop?.AddIdle (() => { - action (); - return false; - }); - } + public static void Invoke (Action action) => MainLoop?.AddIdle (() => { + action (); + return false; + }); // TODO: Determine if this is really needed. The only code that calls WakeUp I can find // is ProgressBarStyles and it's not clear it needs to. @@ -556,7 +554,7 @@ public static void Refresh () } /// - /// This event is raised on each iteration of the . + /// This event is raised on each iteration of the main loop. /// /// /// See also @@ -580,26 +578,20 @@ public static void Refresh () // users use async/await on their code // class MainLoopSyncContext : SynchronizationContext { - public override SynchronizationContext CreateCopy () - { - return new MainLoopSyncContext (); - } + public override SynchronizationContext CreateCopy () => new MainLoopSyncContext (); - public override void Post (SendOrPostCallback d, object state) - { - MainLoop.AddIdle (() => { - d (state); - return false; - }); - //_mainLoop.Driver.Wakeup (); - } + public override void Post (SendOrPostCallback d, object state) => MainLoop.AddIdle (() => { + d (state); + return false; + }); + //_mainLoop.Driver.Wakeup (); public override void Send (SendOrPostCallback d, object state) { if (Thread.CurrentThread.ManagedThreadId == _mainThreadId) { d (state); } else { - var wasExecuted = false; + bool wasExecuted = false; Invoke (() => { d (state); wasExecuted = true; @@ -612,7 +604,7 @@ public override void Send (SendOrPostCallback d, object state) } /// - /// Building block API: Runs the for the created . + /// Building block API: Runs the main loop for the created . /// /// The state returned by the method. public static void RunLoop (RunState state) @@ -624,13 +616,18 @@ public static void RunLoop (RunState state) throw new ObjectDisposedException ("state"); } - var firstIteration = true; + bool firstIteration = true; for (state.Toplevel.Running = true; state.Toplevel.Running;) { + MainLoop.Running = true; if (EndAfterFirstIteration && !firstIteration) { return; } RunIteration (ref state, ref firstIteration); } + MainLoop.Running = false; + // Run one last iteration to consume any outstanding input events from Driver + // This is important for remaining OnKeyUp events. + RunIteration (ref state, ref firstIteration); } /// @@ -641,7 +638,7 @@ public static void RunLoop (RunState state) /// it will be set to if at least one iteration happened. public static void RunIteration (ref RunState state, ref bool firstIteration) { - if (MainLoop.EventsPending ()) { + if (MainLoop.EventsPending () && MainLoop.Running) { // Notify Toplevel it's ready if (firstIteration) { state.Toplevel.OnReady (); @@ -649,7 +646,6 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) MainLoop.RunIteration (); Iteration?.Invoke (null, new IterationEventArgs ()); - EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); if (state.Toplevel != Current) { OverlappedTop?.OnDeactivate (state.Toplevel); @@ -663,7 +659,7 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) firstIteration = false; if (state.Toplevel != Top && - (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { + (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { state.Toplevel.SetNeedsDisplay (state.Toplevel.Frame); Top.Draw (); foreach (var top in _topLevels.Reverse ()) { @@ -675,16 +671,16 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) } } if (_topLevels.Count == 1 && state.Toplevel == Top - && (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height) - && (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded)) { + && (Driver.Cols != state.Toplevel.Frame.Width || Driver.Rows != state.Toplevel.Frame.Height) + && (state.Toplevel.NeedsDisplay || state.Toplevel.SubViewNeedsDisplay || state.Toplevel.LayoutNeeded)) { state.Toplevel.Clear (new Rect (Point.Empty, new Size (Driver.Cols, Driver.Rows))); } if (state.Toplevel.NeedsDisplay || - state.Toplevel.SubViewNeedsDisplay || - state.Toplevel.LayoutNeeded || - OverlappedChildNeedsDisplay ()) { + state.Toplevel.SubViewNeedsDisplay || + state.Toplevel.LayoutNeeded || + OverlappedChildNeedsDisplay ()) { state.Toplevel.Draw (); state.Toplevel.PositionCursor (); Driver.Refresh (); @@ -692,8 +688,8 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) Driver.UpdateCursor (); } if (state.Toplevel != Top && - !state.Toplevel.Modal && - (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { + !state.Toplevel.Modal && + (Top.NeedsDisplay || Top.SubViewNeedsDisplay || Top.LayoutNeeded)) { Top.Draw (); } } @@ -713,12 +709,12 @@ public static void RunIteration (ref RunState state, ref bool firstIteration) /// public static void RequestStop (Toplevel top = null) { - if (OverlappedTop == null || top == null || (OverlappedTop == null && top != null)) { + if (OverlappedTop == null || top == null || OverlappedTop == null && top != null) { top = Current; } if (OverlappedTop != null && top.IsOverlappedContainer && top?.Running == true - && (Current?.Modal == false || (Current?.Modal == true && Current?.Running == false))) { + && (Current?.Modal == false || Current?.Modal == true && Current?.Running == false)) { OverlappedTop.RequestStop (); } else if (OverlappedTop != null && top != Current && Current?.Running == true && Current?.Modal == true @@ -738,10 +734,10 @@ public static void RequestStop (Toplevel top = null) OnNotifyStopRunState (Current); top.Running = false; OnNotifyStopRunState (top); - } else if ((OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false - && Current?.Running == true && !top.Running) - || (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false - && Current?.Running == false && !top.Running && _topLevels.ToArray () [1].Running)) { + } else if (OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false + && Current?.Running == true && !top.Running + || OverlappedTop != null && top != OverlappedTop && top != Current && Current?.Modal == false + && Current?.Running == false && !top.Running && _topLevels.ToArray () [1].Running) { MoveCurrent (top); } else if (OverlappedTop != null && Current != top && Current?.Running == true && !top.Running @@ -757,7 +753,7 @@ public static void RequestStop (Toplevel top = null) OnNotifyStopRunState (Current); } else { Toplevel currentTop; - if (top == Current || (Current?.Modal == true && !top.Modal)) { + if (top == Current || Current?.Modal == true && !top.Modal) { currentTop = Current; } else { currentTop = top; @@ -814,7 +810,7 @@ public static void End (RunState runState) // If there is a OverlappedTop that is not the RunState.Toplevel then runstate.TopLevel // is a child of MidTop and we should notify the OverlappedTop that it is closing - if (OverlappedTop != null && !(runState.Toplevel).Modal && runState.Toplevel != OverlappedTop) { + if (OverlappedTop != null && !runState.Toplevel.Modal && runState.Toplevel != OverlappedTop) { OverlappedTop.OnChildClosed (runState.Toplevel); } @@ -837,11 +833,10 @@ public static void End (RunState runState) runState.Toplevel = null; runState.Dispose (); } - #endregion Run (Begin, Run, End) #region Toplevel handling - static readonly Stack _topLevels = new Stack (); + static readonly Stack _topLevels = new (); /// /// The object used for the application on startup () @@ -858,7 +853,7 @@ public static void End (RunState runState) static void EnsureModalOrVisibleAlwaysOnTop (Toplevel Toplevel) { - if (!Toplevel.Running || (Toplevel == Current && Toplevel.Visible) || OverlappedTop == null || _topLevels.Peek ().Modal) { + if (!Toplevel.Running || Toplevel == Current && Toplevel.Visible || OverlappedTop == null || _topLevels.Peek ().Modal) { return; } @@ -886,8 +881,8 @@ static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int if (_topLevels != null) { int count = _topLevels.Count; if (count > 0) { - var rx = x - startFrame.X; - var ry = y - startFrame.Y; + int rx = x - startFrame.X; + int ry = y - startFrame.Y; foreach (var t in _topLevels) { if (t != Current) { if (t != start && t.Visible && t.Frame.Contains (rx, ry)) { @@ -905,7 +900,7 @@ static View FindDeepestTop (Toplevel start, int x, int y, out int resx, out int static View FindTopFromView (View view) { - View top = view?.SuperView != null && view?.SuperView != Top + var top = view?.SuperView != null && view?.SuperView != Top ? view.SuperView : view; while (top?.SuperView != null && top?.SuperView != Top) { @@ -923,7 +918,7 @@ static bool MoveCurrent (Toplevel top) lock (_topLevels) { _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); } - var index = 0; + int index = 0; var savedToplevels = _topLevels.ToArray (); foreach (var t in savedToplevels) { if (!t.Modal && t != Current && t != top && t != savedToplevels [index]) { @@ -941,7 +936,7 @@ static bool MoveCurrent (Toplevel top) lock (_topLevels) { _topLevels.MoveTo (Current, 0, new ToplevelEqualityComparer ()); } - var index = 0; + int index = 0; foreach (var t in _topLevels.ToArray ()) { if (!t.Running && t != Current && index > 0) { lock (_topLevels) { @@ -952,10 +947,10 @@ static bool MoveCurrent (Toplevel top) } return false; } - if ((OverlappedTop != null && top?.Modal == true && _topLevels.Peek () != top) - || (OverlappedTop != null && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop) - || (OverlappedTop != null && Current?.Modal == false && top != Current) - || (OverlappedTop != null && Current?.Modal == true && top == OverlappedTop)) { + if (OverlappedTop != null && top?.Modal == true && _topLevels.Peek () != top + || OverlappedTop != null && Current != OverlappedTop && Current?.Modal == false && top == OverlappedTop + || OverlappedTop != null && Current?.Modal == false && top != Current + || OverlappedTop != null && Current?.Modal == true && top == OverlappedTop) { lock (_topLevels) { _topLevels.MoveTo (top, 0, new ToplevelEqualityComparer ()); Current = top; @@ -995,7 +990,6 @@ public static bool OnSizeChanging (SizeChangedEventArgs args) Refresh (); return true; } - #endregion Toplevel handling #region Mouse handling @@ -1176,9 +1170,9 @@ public static void OnMouseEvent (MouseEventEventArgs a) } if ((view == null || view == OverlappedTop) && - Current is { Modal: false } && OverlappedTop != null && - a.MouseEvent.Flags != MouseFlags.ReportMousePosition && - a.MouseEvent.Flags != 0) { + Current is { Modal: false } && OverlappedTop != null && + a.MouseEvent.Flags != MouseFlags.ReportMousePosition && + a.MouseEvent.Flags != 0) { var top = FindDeepestTop (Top, a.MouseEvent.X, a.MouseEvent.Y, out _, out _); view = View.FindDeepestView (top, a.MouseEvent.X, a.MouseEvent.Y, out screenX, out screenY); @@ -1294,13 +1288,12 @@ bool FrameHandledMouseEvent (Frame frame) #endregion Mouse handling #region Keyboard handling - - static Key _alternateForwardKey = Key.PageDown | Key.CtrlMask; + static Key _alternateForwardKey = new (KeyCode.PageDown | KeyCode.CtrlMask); /// /// Alternative key to navigate forwards through views. Ctrl+Tab is the primary key. /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))] + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key AlternateForwardKey { get => _alternateForwardKey; set { @@ -1319,12 +1312,12 @@ static void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) } } - static Key _alternateBackwardKey = Key.PageUp | Key.CtrlMask; + static Key _alternateBackwardKey = new (KeyCode.PageUp | KeyCode.CtrlMask); /// /// Alternative key to navigate backwards through views. Shift+Ctrl+Tab is the primary key. /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))] + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key AlternateBackwardKey { get => _alternateBackwardKey; set { @@ -1343,12 +1336,12 @@ static void OnAlternateBackwardKeyChanged (KeyChangedEventArgs oldKey) } } - static Key _quitKey = Key.Q | Key.CtrlMask; + static Key _quitKey = new (KeyCode.Q | KeyCode.CtrlMask); /// /// Gets or sets the key to quit the application. /// - [SerializableConfigurationProperty (Scope = typeof (SettingsScope)), JsonConverter (typeof (KeyJsonConverter))] + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] [JsonConverter (typeof (KeyJsonConverter))] public static Key QuitKey { get => _quitKey; set { @@ -1359,6 +1352,7 @@ public static Key QuitKey { } } } + static void OnQuitKeyChanged (KeyChangedEventArgs e) { // Duplicate the list so if it changes during enumeration we're safe @@ -1368,115 +1362,119 @@ static void OnQuitKeyChanged (KeyChangedEventArgs e) } /// - /// Event fired after a key has been pressed and released. - /// Set to to suppress the event. + /// Event fired when the user presses a key. Fired by . + /// + /// Set to to indicate the key was handled and + /// to prevent additional processing. + /// /// /// - /// All drivers support firing the event. Some drivers (Curses) + /// All drivers support firing the event. Some drivers (Curses) /// do not support firing the and events. + /// + /// Fired after and before . + /// /// - public static event EventHandler KeyPressed; + public static event EventHandler KeyDown; /// - /// Called after a key has been pressed and released. Fires the event. - /// - /// Called for new KeyPressed events before any processing is performed or - /// views evaluate. Use for global key handling and/or debugging. - /// + /// Called by the when the user presses a key. + /// Fires the event + /// then calls on all top level views. + /// Called after and before . /// - /// + /// + /// Can be used to simulate key press events. + /// + /// /// if the key was handled. - public static bool OnKeyPressed (KeyEventEventArgs a) + public static bool OnKeyDown (Key keyEvent) { - KeyPressed?.Invoke (null, a); - if (a.Handled) { + if (!_initialized) { return true; } - var chain = _topLevels.ToList (); - foreach (var topLevel in chain) { - if (topLevel.ProcessHotKey (a.KeyEvent)) { - return true; - } - if (topLevel.Modal) - break; + KeyDown?.Invoke (null, keyEvent); + if (keyEvent.Handled) { + return true; } - foreach (var topLevel in chain) { - if (topLevel.ProcessKey (a.KeyEvent)) { + foreach (var topLevel in _topLevels.ToList ()) { + if (topLevel.NewKeyDownEvent (keyEvent)) { return true; } - if (topLevel.Modal) + if (topLevel.Modal) { break; + } } - foreach (var topLevel in chain) { - // Process the key normally - if (topLevel.ProcessColdKey (a.KeyEvent)) { - return true; + // Invoke any Global KeyBindings + foreach (var topLevel in _topLevels.ToList ()) { + foreach (var view in topLevel.Subviews.Where (v => v.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out var _))) { + if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.Application, out var _)) { + keyEvent.Scope = KeyBindingScope.Application; + bool? handled = view.OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + return true; + } + } } - if (topLevel.Modal) - break; } + return false; } /// - /// Event fired when a key is pressed (and not yet released). + /// Event fired when the user releases a key. Fired by . + /// + /// Set to to indicate the key was handled and + /// to prevent additional processing. + /// /// /// - /// All drivers support firing the event. Some drivers (Curses) + /// All drivers support firing the event. Some drivers (Curses) /// do not support firing the and events. + /// + /// Fired after . + /// /// - public static event EventHandler KeyDown; + public static event EventHandler KeyUp; /// - /// Called when a key is pressed (and not yet released). Fires the event. - /// - /// - public static void OnKeyDown (KeyEventEventArgs a) - { - KeyDown?.Invoke (null, a); - var chain = _topLevels.ToList (); - foreach (var topLevel in chain) { - if (topLevel.OnKeyDown (a.KeyEvent)) - return; - if (topLevel.Modal) - break; - } - } - - /// - /// Event fired when a key is released. + /// Called by the when the user releases a key. + /// Fires the event + /// then calls on all top level views. + /// Called after . /// /// - /// All drivers support firing the event. Some drivers (Curses) - /// do not support firing the and events. + /// Can be used to simulate key press events. /// - public static event EventHandler KeyUp; - - /// - /// Called when a key is released. Fires the event. - /// /// - public static void OnKeyUp (KeyEventEventArgs a) + /// if the key was handled. + public static bool OnKeyUp (Key a) { + if (!_initialized) { + return true; + } + KeyUp?.Invoke (null, a); - var chain = _topLevels.ToList (); - foreach (var topLevel in chain) { - if (topLevel.OnKeyUp (a.KeyEvent)) - return; - if (topLevel.Modal) + if (a.Handled) { + return true; + } + foreach (var topLevel in _topLevels.ToList ()) { + if (topLevel.NewKeyUpEvent (a)) { + return true; + } + if (topLevel.Modal) { break; + } } - + return false; } - #endregion Keyboard handling } /// /// Event arguments for the event. /// -public class IterationEventArgs { -} \ No newline at end of file +public class IterationEventArgs { } \ No newline at end of file diff --git a/Terminal.Gui/Clipboard/Clipboard.cs b/Terminal.Gui/Clipboard/Clipboard.cs index 47de47ab60..802f7e1136 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -58,8 +58,6 @@ public static string Contents { Application.Driver.Clipboard.SetClipboardData (value); } _contents = value; - } catch (NotSupportedException e) { - throw e; } catch (Exception) { _contents = value; } diff --git a/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs b/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs new file mode 100644 index 0000000000..052c4c4b3d --- /dev/null +++ b/Terminal.Gui/Configuration/KeyCodeJsonConverter.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Terminal.Gui; +class KeyCodeJsonConverter : JsonConverter { + public override KeyCode Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.StartObject) { + KeyCode key = KeyCode.Unknown; + Dictionary modifierDict = new Dictionary (comparer: StringComparer.InvariantCultureIgnoreCase) { + { "Shift", KeyCode.ShiftMask }, + { "Ctrl", KeyCode.CtrlMask }, + { "Alt", KeyCode.AltMask } + }; + + List modifiers = new List (); + + while (reader.Read ()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } + + if (reader.TokenType == JsonTokenType.PropertyName) { + string propertyName = reader.GetString (); + reader.Read (); + + switch (propertyName.ToLowerInvariant ()) { + case "key": + if (reader.TokenType == JsonTokenType.String) { + if (Enum.TryParse (reader.GetString (), false, out key)) { + break; + } + + // The enum uses "D0..D9" for the number keys + if (Enum.TryParse (reader.GetString ().TrimStart ('D', 'd'), false, out key)) { + break; + } + + if (key == KeyCode.Unknown || key == KeyCode.Null) { + throw new JsonException ($"The value \"{reader.GetString ()}\" is not a valid Key."); + } + + } else if (reader.TokenType == JsonTokenType.Number) { + try { + key = (KeyCode)reader.GetInt32 (); + } catch (InvalidOperationException ioe) { + throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe); + } catch (FormatException ioe) { + throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe); + } + break; + } + break; + + case "modifiers": + if (reader.TokenType == JsonTokenType.StartArray) { + while (reader.Read ()) { + if (reader.TokenType == JsonTokenType.EndArray) { + break; + } + var mod = reader.GetString (); + try { + modifiers.Add (modifierDict [mod]); + } catch (KeyNotFoundException e) { + throw new JsonException ($"The value \"{mod}\" is not a valid modifier.", e); + } + } + } else { + throw new JsonException ($"Expected an array of modifiers, but got \"{reader.TokenType}\"."); + } + break; + + default: + throw new JsonException ($"Unexpected Key property \"{propertyName}\"."); + } + } + } + + foreach (var modifier in modifiers) { + key |= modifier; + } + + return key; + } + throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}."); + } + + public override void Write (Utf8JsonWriter writer, KeyCode value, JsonSerializerOptions options) + { + writer.WriteStartObject (); + + var keyName = (value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask).ToString (); + if (keyName != null) { + writer.WriteString ("Key", keyName); + } else { + writer.WriteNumber ("Key", (uint)(value & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask)); + } + + Dictionary modifierDict = new Dictionary + { + { "Shift", KeyCode.ShiftMask }, + { "Ctrl", KeyCode.CtrlMask }, + { "Alt", KeyCode.AltMask } + }; + + List modifiers = new List (); + foreach (var pair in modifierDict) { + if ((value & pair.Value) == pair.Value) { + modifiers.Add (pair.Key); + } + } + + if (modifiers.Count > 0) { + writer.WritePropertyName ("Modifiers"); + writer.WriteStartArray (); + foreach (var modifier in modifiers) { + writer.WriteStringValue (modifier); + } + writer.WriteEndArray (); + } + + writer.WriteEndObject (); + } +} diff --git a/Terminal.Gui/Configuration/KeyJsonConverter.cs b/Terminal.Gui/Configuration/KeyJsonConverter.cs index 3474c16d62..f292055b3a 100644 --- a/Terminal.Gui/Configuration/KeyJsonConverter.cs +++ b/Terminal.Gui/Configuration/KeyJsonConverter.cs @@ -1,127 +1,48 @@ using System; -using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization; -namespace Terminal.Gui { - class KeyJsonConverter : JsonConverter { - public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.StartObject) { - Key key = Key.Unknown; - Dictionary modifierDict = new Dictionary (comparer: StringComparer.InvariantCultureIgnoreCase) { - { "Shift", Key.ShiftMask }, - { "Ctrl", Key.CtrlMask }, - { "Alt", Key.AltMask } - }; - - List modifiers = new List (); - - while (reader.Read ()) { - if (reader.TokenType == JsonTokenType.EndObject) { - break; - } - - if (reader.TokenType == JsonTokenType.PropertyName) { - string propertyName = reader.GetString (); - reader.Read (); - - switch (propertyName.ToLowerInvariant ()) { - case "key": - if (reader.TokenType == JsonTokenType.String) { - if (Enum.TryParse (reader.GetString (), false, out key)) { - break; - } - - // The enum uses "D0..D9" for the number keys - if (Enum.TryParse (reader.GetString ().TrimStart ('D', 'd'), false, out key)) { - break; - } +namespace Terminal.Gui; +class KeyJsonConverter : JsonConverter { + + public override Key Read (ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.StartObject) { + Key key = KeyCode.Unknown; + while (reader.Read ()) { + if (reader.TokenType == JsonTokenType.EndObject) { + break; + } - if (key == Key.Unknown || key == Key.Null) { - throw new JsonException ($"The value \"{reader.GetString ()}\" is not a valid Key."); - } + if (reader.TokenType == JsonTokenType.PropertyName) { + string propertyName = reader.GetString (); + reader.Read (); - } else if (reader.TokenType == JsonTokenType.Number) { - try { - key = (Key)reader.GetInt32 (); - } catch (InvalidOperationException ioe) { - throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe); - } catch (FormatException ioe) { - throw new JsonException ($"Error parsing Key value: {ioe.Message}", ioe); - } + switch (propertyName?.ToLowerInvariant ()) { + case "key": + if (reader.TokenType == JsonTokenType.String) { + string keyValue = reader.GetString (); + if (Key.TryParse (keyValue, out key)) { break; } - break; + throw new JsonException ($"Error parsing Key: {keyValue}."); - case "modifiers": - if (reader.TokenType == JsonTokenType.StartArray) { - while (reader.Read ()) { - if (reader.TokenType == JsonTokenType.EndArray) { - break; - } - var mod = reader.GetString (); - try { - modifiers.Add (modifierDict [mod]); - } catch (KeyNotFoundException e) { - throw new JsonException ($"The value \"{mod}\" is not a valid modifier.", e); - } - } - } else { - throw new JsonException ($"Expected an array of modifiers, but got \"{reader.TokenType}\"."); - } - break; - - default: - throw new JsonException ($"Unexpected Key property \"{propertyName}\"."); } + break; + default: + throw new JsonException ($"Unexpected Key property \"{propertyName}\"."); } } - - foreach (var modifier in modifiers) { - key |= modifier; - } - - return key; } - throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}."); + return key; } + throw new JsonException ($"Unexpected StartObject token when parsing Key: {reader.TokenType}."); + } - public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options) - { - writer.WriteStartObject (); - - var keyName = (value & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask).ToString (); - if (keyName != null) { - writer.WriteString ("Key", keyName); - } else { - writer.WriteNumber ("Key", (uint)(value & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask)); - } - - Dictionary modifierDict = new Dictionary - { - { "Shift", Key.ShiftMask }, - { "Ctrl", Key.CtrlMask }, - { "Alt", Key.AltMask } - }; - - List modifiers = new List (); - foreach (var pair in modifierDict) { - if ((value & pair.Value) == pair.Value) { - modifiers.Add (pair.Key); - } - } - - if (modifiers.Count > 0) { - writer.WritePropertyName ("Modifiers"); - writer.WriteStartArray (); - foreach (var modifier in modifiers) { - writer.WriteStringValue (modifier); - } - writer.WriteEndArray (); - } - - writer.WriteEndObject (); - } + public override void Write (Utf8JsonWriter writer, Key value, JsonSerializerOptions options) + { + writer.WriteStartObject (); + writer.WriteString ("Key", value.ToString ()); + writer.WriteEndObject (); } } diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs index 0a92e0274d..31d448a963 100644 --- a/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs @@ -3,11 +3,8 @@ // using System.Text; using System; -using System.Collections.Generic; using System.Diagnostics; -using static Terminal.Gui.ColorScheme; using System.Linq; -using System.Data; namespace Terminal.Gui; @@ -88,18 +85,9 @@ public abstract class ConsoleDriver { /// The contents of the application output. The driver outputs this buffer to the terminal when /// is called. /// - /// The format of the array is rows, columns, and 3 values on the last column: Rune, Attribute and Dirty Flag + /// The format of the array is rows, columns. The first index is the row, the second index is the column. /// /// - //public int [,,] Contents { get; internal set; } - - ///// - ///// The contents of the application output. The driver outputs this buffer to the terminal when - ///// is called. - ///// - ///// The format of the array is rows, columns. The first index is the row, the second index is the column. - ///// - ///// public Cell [,] Contents { get; internal set; } /// @@ -473,39 +461,33 @@ public virtual Attribute MakeColor (Color foreground, Color background) #endregion #region Mouse and Keyboard - /// - /// Event fired after a key has been pressed and released. + /// Event fired when a key is pressed down. This is a precursor to . /// - public event EventHandler KeyPressed; + public event EventHandler KeyDown; /// - /// Called after a key has been pressed and released. Fires the event. + /// Called when a key is pressed down. Fires the event. This is a precursor to . /// /// - public void OnKeyPressed (KeyEventEventArgs a) => KeyPressed?.Invoke (this, a); + public void OnKeyDown (Key a) => KeyDown?.Invoke (this, a); /// - /// Event fired when a key is released. + /// Event fired when a key is released. /// - public event EventHandler KeyUp; + /// + /// Drivers that do not support key release events will fire this event after processing is complete. + /// + public event EventHandler KeyUp; /// /// Called when a key is released. Fires the event. /// + /// + /// Drivers that do not support key release events will calls this method after processing is complete. + /// /// - public void OnKeyUp (KeyEventEventArgs a) => KeyUp?.Invoke (this, a); - - /// - /// Event fired when a key is pressed. - /// - public event EventHandler KeyDown; - - /// - /// Called when a key is pressed. Fires the event. - /// - /// - public void OnKeyDown (KeyEventEventArgs a) => KeyDown?.Invoke (this, a); + public void OnKeyUp (Key a) => KeyUp?.Invoke (this, a); /// /// Event fired when a mouse event occurs. @@ -651,3 +633,437 @@ public enum CursorVisibility { /// Works under Xterm-like terminal otherwise this is equivalent to BoxFix = 0x02020164, } + + +/// +/// The enumeration encodes key information from s and provides a consistent +/// way for application code to specify keys and receive key events. +/// +/// The class provides a higher-level abstraction, with helper methods and properties for common +/// operations. For example, and provide a convenient way +/// to check whether the Alt or Ctrl modifier keys were pressed when a key was pressed. +/// +/// +/// +/// +/// Lowercase alpha keys are encoded as values between 65 and 90 corresponding to the un-shifted A to Z keys on a keyboard. Enum values +/// are provided for these (e.g. , , etc.). Even though the values are the same as the ASCII +/// values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. +/// TODO: Strongly consider renaming these from .A to .Z to .A_Lowercase to .Z_Lowercase (or .a to .z). +/// +/// +/// Numeric keys are the values between 48 and 57 corresponding to 0 to 9 (e.g. , , etc.). +/// +/// +/// The shift modifiers (, , and ) can be combined (with logical or) +/// with the other key codes to represent shifted keys. For example, the enum value represents the un-shifted 'a' key, while +/// | represents the 'A' key (shifted 'a' key). Likewise, | +/// represents the 'Alt+A' key combination. +/// +/// +/// All other keys that produce a printable character are encoded as the Unicode value of the character. For example, the +/// for the '!' character is 33, which is the Unicode value for '!'. Likewise, `â` is 226, `Â` is 194, etc. +/// +/// +/// If the is set, then the value is that of the special mask, +/// otherwise, the value is the one of the lower bits (as extracted by ). +/// +/// +[Flags] +public enum KeyCode : uint { + /// + /// Mask that indicates that this is a character value, values outside this range + /// indicate special characters like Alt-key combinations or special keys on the + /// keyboard like function keys, arrows keys and so on. + /// + CharMask = 0xfffff, + + /// + /// If the is set, then the value is that of the special mask, + /// otherwise, the value is the one of the lower bits (as extracted by ). + /// + SpecialMask = 0xfff00000, + + /// + /// The key code representing null or empty + /// + Null = '\0', + + /// + /// Backspace key. + /// + Backspace = 8, + + /// + /// The key code for the tab key (forwards tab key). + /// + Tab = 9, + + /// + /// The key code for the return key. + /// + Enter = '\n', + + /// + /// The key code for the clear key. + /// + Clear = 12, + + /// + /// The key code for the Shift key. + /// + ShiftKey = 16, + + /// + /// The key code for the Ctrl key. + /// + CtrlKey = 17, + + /// + /// The key code for the Alt key. + /// + AltKey = 18, + + /// + /// The key code for the CapsLock key. + /// + CapsLock = 20, + + ///// + ///// The key code for the NumLock key. + ///// + //NumLock = 144, + + ///// + ///// The key code for the ScrollLock key. + ///// + //ScrollLock = 145, + + /// + /// The key code for the escape key. + /// + Esc = 27, + + /// + /// The key code for the space bar key. + /// + Space = 32, + + /// + /// Digit 0. + /// + D0 = 48, + /// + /// Digit 1. + /// + D1, + /// + /// Digit 2. + /// + D2, + /// + /// Digit 3. + /// + D3, + /// + /// Digit 4. + /// + D4, + /// + /// Digit 5. + /// + D5, + /// + /// Digit 6. + /// + D6, + /// + /// Digit 7. + /// + D7, + /// + /// Digit 8. + /// + D8, + /// + /// Digit 9. + /// + D9, + + /// + /// The key code for the user pressing Shift-A + /// + A = 65, + /// + /// The key code for the user pressing Shift-B + /// + B, + /// + /// The key code for the user pressing Shift-C + /// + C, + /// + /// The key code for the user pressing Shift-D + /// + D, + /// + /// The key code for the user pressing Shift-E + /// + E, + /// + /// The key code for the user pressing Shift-F + /// + F, + /// + /// The key code for the user pressing Shift-G + /// + G, + /// + /// The key code for the user pressing Shift-H + /// + H, + /// + /// The key code for the user pressing Shift-I + /// + I, + /// + /// The key code for the user pressing Shift-J + /// + J, + /// + /// The key code for the user pressing Shift-K + /// + K, + /// + /// The key code for the user pressing Shift-L + /// + L, + /// + /// The key code for the user pressing Shift-M + /// + M, + /// + /// The key code for the user pressing Shift-N + /// + N, + /// + /// The key code for the user pressing Shift-O + /// + O, + /// + /// The key code for the user pressing Shift-P + /// + P, + /// + /// The key code for the user pressing Shift-Q + /// + Q, + /// + /// The key code for the user pressing Shift-R + /// + R, + /// + /// The key code for the user pressing Shift-S + /// + S, + /// + /// The key code for the user pressing Shift-T + /// + T, + /// + /// The key code for the user pressing Shift-U + /// + U, + /// + /// The key code for the user pressing Shift-V + /// + V, + /// + /// The key code for the user pressing Shift-W + /// + W, + /// + /// The key code for the user pressing Shift-X + /// + X, + /// + /// The key code for the user pressing Shift-Y + /// + Y, + /// + /// The key code for the user pressing Shift-Z + /// + Z, + /// + /// The key code for the user pressing A + /// + Delete = 127, + + /// + /// When this value is set, the Key encodes the sequence Shift-KeyValue. + /// + ShiftMask = 0x10000000, + + /// + /// When this value is set, the Key encodes the sequence Alt-KeyValue. + /// And the actual value must be extracted by removing the AltMask. + /// + AltMask = 0x80000000, + + /// + /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. + /// And the actual value must be extracted by removing the CtrlMask. + /// + CtrlMask = 0x40000000, + + /// + /// Cursor up key + /// + CursorUp = 0x100000, + /// + /// Cursor down key. + /// + CursorDown, + /// + /// Cursor left key. + /// + CursorLeft, + /// + /// Cursor right key. + /// + CursorRight, + /// + /// Page Up key. + /// + PageUp, + /// + /// Page Down key. + /// + PageDown, + /// + /// Home key. + /// + Home, + /// + /// End key. + /// + End, + + /// + /// Insert character key. + /// + InsertChar, + + /// + /// Delete character key. + /// + DeleteChar, + + /// + /// Print screen character key. + /// + PrintScreen, + + /// + /// F1 key. + /// + F1, + /// + /// F2 key. + /// + F2, + /// + /// F3 key. + /// + F3, + /// + /// F4 key. + /// + F4, + /// + /// F5 key. + /// + F5, + /// + /// F6 key. + /// + F6, + /// + /// F7 key. + /// + F7, + /// + /// F8 key. + /// + F8, + /// + /// F9 key. + /// + F9, + /// + /// F10 key. + /// + F10, + /// + /// F11 key. + /// + F11, + /// + /// F12 key. + /// + F12, + /// + /// F13 key. + /// + F13, + /// + /// F14 key. + /// + F14, + /// + /// F15 key. + /// + F15, + /// + /// F16 key. + /// + F16, + /// + /// F17 key. + /// + F17, + /// + /// F18 key. + /// + F18, + /// + /// F19 key. + /// + F19, + /// + /// F20 key. + /// + F20, + /// + /// F21 key. + /// + F21, + /// + /// F22 key. + /// + F22, + /// + /// F23 key. + /// + F23, + /// + /// F24 key. + /// + F24, + + /// + /// A key with an unknown mapping was raised. + /// + Unknown +} + diff --git a/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs new file mode 100644 index 0000000000..3c9d00da27 --- /dev/null +++ b/Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs @@ -0,0 +1,601 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; + +namespace Terminal.Gui.ConsoleDrivers { + /// + /// Helper class to handle the scan code and virtual key from a . + /// + public static class ConsoleKeyMapping { + class ScanCodeMapping : IEquatable { + public uint ScanCode; + public uint VirtualKey; + public ConsoleModifiers Modifiers; + public uint UnicodeChar; + + public ScanCodeMapping (uint scanCode, uint virtualKey, ConsoleModifiers modifiers, uint unicodeChar) + { + ScanCode = scanCode; + VirtualKey = virtualKey; + Modifiers = modifiers; + UnicodeChar = unicodeChar; + } + + public bool Equals (ScanCodeMapping other) + { + return ScanCode.Equals (other.ScanCode) && + VirtualKey.Equals (other.VirtualKey) && + Modifiers.Equals (other.Modifiers) && + UnicodeChar.Equals (other.UnicodeChar); + } + } + + static ConsoleModifiers GetModifiers (ConsoleModifiers modifiers) + { + if (modifiers.HasFlag (ConsoleModifiers.Shift) + && !modifiers.HasFlag (ConsoleModifiers.Alt) + && !modifiers.HasFlag (ConsoleModifiers.Control)) { + return ConsoleModifiers.Shift; + } else if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return modifiers; + } + + return 0; + } + + static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers) + { + switch (propName) { + case "UnicodeChar": + var sCode = scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == modifiers); + if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == 0); + } + return sCode; + case "VirtualKey": + sCode = scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == modifiers); + if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { + return scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == 0); + } + return sCode; + } + + return null; + } + + /// + /// Gets the from the provided . + /// + /// + /// + public static ConsoleKeyInfo GetConsoleKeyFromKey (KeyCode key) + { + var mod = new ConsoleModifiers (); + if (key.HasFlag (KeyCode.ShiftMask)) { + mod |= ConsoleModifiers.Shift; + } + if (key.HasFlag (KeyCode.AltMask)) { + mod |= ConsoleModifiers.Alt; + } + if (key.HasFlag (KeyCode.CtrlMask)) { + mod |= ConsoleModifiers.Control; + } + return GetConsoleKeyFromKey ((uint)(key & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask), mod, out _); + } + + /// + /// Get the from a unicode character and modifiers (e.g. (Key)'a' and (Key)Key.CtrlMask). + /// + /// The key as a unicode codepoint. + /// The modifier keys. + /// The resulting scan code. + /// The . + public static ConsoleKeyInfo GetConsoleKeyFromKey (uint keyValue, ConsoleModifiers modifiers, out uint scanCode) + { + scanCode = 0; + uint outputChar = keyValue; + if (keyValue == 0) { + return new ConsoleKeyInfo ((char)keyValue, ConsoleKey.None, modifiers.HasFlag (ConsoleModifiers.Shift), + modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control)); + } + + uint consoleKey = (uint)MapKeyToConsoleKey ((KeyCode)keyValue, modifiers, out bool mappable); + if (mappable) { + var mod = GetModifiers (modifiers); + var scode = GetScanCode ("UnicodeChar", keyValue, mod); + if (scode != null) { + consoleKey = scode.VirtualKey; + scanCode = scode.ScanCode; + outputChar = scode.UnicodeChar; + } else { + // If the consoleKey is < 255, retain the lower 8 bits of the key value and set the upper bits to 0xff. + // This is a shifted value that will be used by the GetKeyCharFromConsoleKey to do the correct action + // because keyValue maybe a UnicodeChar or a ConsoleKey, e.g. for PageUp is passed the ConsoleKey.PageUp + consoleKey = consoleKey < 0xff ? consoleKey & 0xff | 0xff << 8 : consoleKey; + outputChar = GetKeyCharFromConsoleKey (consoleKey, modifiers, out consoleKey, out scanCode); + } + } else { + var mod = GetModifiers (modifiers); + var scode = GetScanCode ("VirtualKey", consoleKey, mod); + if (scode != null) { + consoleKey = scode.VirtualKey; + scanCode = scode.ScanCode; + outputChar = scode.UnicodeChar; + } + } + + return new ConsoleKeyInfo ((char)outputChar, (ConsoleKey)consoleKey, modifiers.HasFlag (ConsoleModifiers.Shift), + modifiers.HasFlag (ConsoleModifiers.Alt), modifiers.HasFlag (ConsoleModifiers.Control)); + } + + /// + /// Get the output character from the , the correct + /// and the scan code used on . + /// + /// The unicode character. + /// The modifiers keys. + /// The resulting console key. + /// The resulting scan code. + /// The output character or the . + static uint GetKeyCharFromConsoleKey (uint unicodeChar, ConsoleModifiers modifiers, out uint consoleKey, out uint scanCode) + { + uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar; + uint keyChar = decodedChar; + consoleKey = 0; + var mod = GetModifiers (modifiers); + scanCode = 0; + var scode = unicodeChar != 0 && unicodeChar >> 8 != 0xff ? GetScanCode ("VirtualKey", decodedChar, mod) : null; + if (scode != null) { + consoleKey = scode.VirtualKey; + keyChar = scode.UnicodeChar; + scanCode = scode.ScanCode; + } + if (scode == null) { + scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null; + if (scode != null) { + consoleKey = scode.VirtualKey; + keyChar = scode.UnicodeChar; + scanCode = scode.ScanCode; + } + } + if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar)) { + string stFormD = ((char)decodedChar).ToString ().Normalize (System.Text.NormalizationForm.FormD); + for (int i = 0; i < stFormD.Length; i++) { + var uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]); + if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) { + consoleKey = char.ToUpper (stFormD [i]); + scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0); + if (scode != null) { + scanCode = scode.ScanCode; + } + } + } + } + + return keyChar; + } + + /// + /// Maps a unicode character (e.g. (Key)'a') to a uint representing a . + /// + /// The key value. + /// The modifiers keys. + /// + /// means the return value can be mapped to a valid unicode character. + /// means the return value is in the ConsoleKey enum. + /// + /// The or the . + public static ConsoleKey MapKeyToConsoleKey (KeyCode keyValue, ConsoleModifiers modifiers, out bool isMappable) + { + isMappable = false; + + switch (keyValue) { + case KeyCode.Delete: + return ConsoleKey.Delete; + case KeyCode.CursorUp: + return ConsoleKey.UpArrow; + case KeyCode.CursorDown: + return ConsoleKey.DownArrow; + case KeyCode.CursorLeft: + return ConsoleKey.LeftArrow; + case KeyCode.CursorRight: + return ConsoleKey.RightArrow; + case KeyCode.PageUp: + return ConsoleKey.PageUp; + case KeyCode.PageDown: + return ConsoleKey.PageDown; + case KeyCode.Home: + return ConsoleKey.Home; + case KeyCode.End: + return ConsoleKey.End; + case KeyCode.InsertChar: + return ConsoleKey.Insert; + case KeyCode.DeleteChar: + return ConsoleKey.Delete; + case KeyCode.F1: + return ConsoleKey.F1; + case KeyCode.F2: + return ConsoleKey.F2; + case KeyCode.F3: + return ConsoleKey.F3; + case KeyCode.F4: + return ConsoleKey.F4; + case KeyCode.F5: + return ConsoleKey.F5; + case KeyCode.F6: + return ConsoleKey.F6; + case KeyCode.F7: + return ConsoleKey.F7; + case KeyCode.F8: + return ConsoleKey.F8; + case KeyCode.F9: + return ConsoleKey.F9; + case KeyCode.F10: + return ConsoleKey.F10; + case KeyCode.F11: + return ConsoleKey.F11; + case KeyCode.F12: + return ConsoleKey.F12; + case KeyCode.F13: + return ConsoleKey.F13; + case KeyCode.F14: + return ConsoleKey.F14; + case KeyCode.F15: + return ConsoleKey.F15; + case KeyCode.F16: + return ConsoleKey.F16; + case KeyCode.F17: + return ConsoleKey.F17; + case KeyCode.F18: + return ConsoleKey.F18; + case KeyCode.F19: + return ConsoleKey.F19; + case KeyCode.F20: + return ConsoleKey.F20; + case KeyCode.F21: + return ConsoleKey.F21; + case KeyCode.F22: + return ConsoleKey.F22; + case KeyCode.F23: + return ConsoleKey.F23; + case KeyCode.F24: + return ConsoleKey.F24; + case KeyCode.Tab | KeyCode.ShiftMask: + return ConsoleKey.Tab; + case KeyCode.Unknown: + isMappable = true; + return 0; + } + + isMappable = true; + + if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= KeyCode.A and <= KeyCode.Z) { + return (ConsoleKey)(keyValue - 32); + } else if (modifiers == ConsoleModifiers.None && keyValue is >= KeyCode.A and <= KeyCode.Z) { + return (ConsoleKey)(keyValue + 32); + } + if (modifiers == ConsoleModifiers.Shift && keyValue - 32 is >= (KeyCode)'À' and <= (KeyCode)'Ý') { + return (ConsoleKey)(keyValue - 32); + } else if (modifiers == ConsoleModifiers.None && keyValue is >= (KeyCode)'À' and <= (KeyCode)'Ý') { + return (ConsoleKey)(keyValue + 32); + } + + return (ConsoleKey)keyValue; + } + + /// + /// Maps a to a . + /// + /// The console key. + /// If is mapped to a valid character, otherwise . + /// The or the . + public static KeyCode MapConsoleKeyToKey (ConsoleKey consoleKey, out bool isMappable) + { + isMappable = false; + + switch (consoleKey) { + case ConsoleKey.Delete: + return KeyCode.Delete; + case ConsoleKey.UpArrow: + return KeyCode.CursorUp; + case ConsoleKey.DownArrow: + return KeyCode.CursorDown; + case ConsoleKey.LeftArrow: + return KeyCode.CursorLeft; + case ConsoleKey.RightArrow: + return KeyCode.CursorRight; + case ConsoleKey.PageUp: + return KeyCode.PageUp; + case ConsoleKey.PageDown: + return KeyCode.PageDown; + case ConsoleKey.Home: + return KeyCode.Home; + case ConsoleKey.End: + return KeyCode.End; + case ConsoleKey.Insert: + return KeyCode.InsertChar; + case ConsoleKey.F1: + return KeyCode.F1; + case ConsoleKey.F2: + return KeyCode.F2; + case ConsoleKey.F3: + return KeyCode.F3; + case ConsoleKey.F4: + return KeyCode.F4; + case ConsoleKey.F5: + return KeyCode.F5; + case ConsoleKey.F6: + return KeyCode.F6; + case ConsoleKey.F7: + return KeyCode.F7; + case ConsoleKey.F8: + return KeyCode.F8; + case ConsoleKey.F9: + return KeyCode.F9; + case ConsoleKey.F10: + return KeyCode.F10; + case ConsoleKey.F11: + return KeyCode.F11; + case ConsoleKey.F12: + return KeyCode.F12; + case ConsoleKey.F13: + return KeyCode.F13; + case ConsoleKey.F14: + return KeyCode.F14; + case ConsoleKey.F15: + return KeyCode.F15; + case ConsoleKey.F16: + return KeyCode.F16; + case ConsoleKey.F17: + return KeyCode.F17; + case ConsoleKey.F18: + return KeyCode.F18; + case ConsoleKey.F19: + return KeyCode.F19; + case ConsoleKey.F20: + return KeyCode.F20; + case ConsoleKey.F21: + return KeyCode.F21; + case ConsoleKey.F22: + return KeyCode.F22; + case ConsoleKey.F23: + return KeyCode.F23; + case ConsoleKey.F24: + return KeyCode.F24; + case ConsoleKey.Tab: + return KeyCode.Tab; + } + isMappable = true; + + if (consoleKey is >= ConsoleKey.A and <= ConsoleKey.Z) { + return (KeyCode)(consoleKey + 32); + } + + return (KeyCode)consoleKey; + } + + /// + /// Maps a to a . + /// + /// The console key info. + /// The key. + /// The with or the + public static KeyCode MapKeyModifiers (ConsoleKeyInfo keyInfo, KeyCode key) + { + var keyMod = new KeyCode (); + if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) { + keyMod = KeyCode.ShiftMask; + } + if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) { + keyMod |= KeyCode.CtrlMask; + } + if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) { + keyMod |= KeyCode.AltMask; + } + + return keyMod != KeyCode.Null ? keyMod | key : key; + } + + static HashSet scanCodes = new HashSet { + new ScanCodeMapping (1, 27, 0, 27), // Escape + new ScanCodeMapping (1, 27, ConsoleModifiers.Shift, 27), + new ScanCodeMapping (2, 49, 0, 49), // D1 + new ScanCodeMapping (2, 49, ConsoleModifiers.Shift, 33), + new ScanCodeMapping (3, 50, 0, 50), // D2 + new ScanCodeMapping (3, 50, ConsoleModifiers.Shift, 34), + new ScanCodeMapping (3, 50, ConsoleModifiers.Alt | ConsoleModifiers.Control, 64), + new ScanCodeMapping (4, 51, 0, 51), // D3 + new ScanCodeMapping (4, 51, ConsoleModifiers.Shift, 35), + new ScanCodeMapping (4, 51, ConsoleModifiers.Alt | ConsoleModifiers.Control, 163), + new ScanCodeMapping (5, 52, 0, 52), // D4 + new ScanCodeMapping (5, 52, ConsoleModifiers.Shift, 36), + new ScanCodeMapping (5, 52, ConsoleModifiers.Alt | ConsoleModifiers.Control, 167), + new ScanCodeMapping (6, 53, 0, 53), // D5 + new ScanCodeMapping (6, 53, ConsoleModifiers.Shift, 37), + new ScanCodeMapping (6, 53, ConsoleModifiers.Alt | ConsoleModifiers.Control, 8364), + new ScanCodeMapping (7, 54, 0, 54), // D6 + new ScanCodeMapping (7, 54, ConsoleModifiers.Shift, 38), + new ScanCodeMapping (8, 55, 0, 55), // D7 + new ScanCodeMapping (8, 55, ConsoleModifiers.Shift, 47), + new ScanCodeMapping (8, 55, ConsoleModifiers.Alt | ConsoleModifiers.Control, 123), + new ScanCodeMapping (9, 56, 0, 56), // D8 + new ScanCodeMapping (9, 56, ConsoleModifiers.Shift, 40), + new ScanCodeMapping (9, 56, ConsoleModifiers.Alt | ConsoleModifiers.Control, 91), + new ScanCodeMapping (10, 57, 0, 57), // D9 + new ScanCodeMapping (10, 57, ConsoleModifiers.Shift, 41), + new ScanCodeMapping (10, 57, ConsoleModifiers.Alt | ConsoleModifiers.Control, 93), + new ScanCodeMapping (11, 48, 0, 48), // D0 + new ScanCodeMapping (11, 48, ConsoleModifiers.Shift, 61), + new ScanCodeMapping (11, 48, ConsoleModifiers.Alt | ConsoleModifiers.Control, 125), + new ScanCodeMapping (12, 219, 0, 39), // Oem4 + new ScanCodeMapping (12, 219, ConsoleModifiers.Shift, 63), + new ScanCodeMapping (13, 221, 0, 171), // Oem6 + new ScanCodeMapping (13, 221, ConsoleModifiers.Shift, 187), + new ScanCodeMapping (14, 8, 0, 8), // Backspace + new ScanCodeMapping (14, 8, ConsoleModifiers.Shift, 8), + new ScanCodeMapping (15, 9, 0, 9), // Tab + new ScanCodeMapping (15, 9, ConsoleModifiers.Shift, 15), + new ScanCodeMapping (16, 81, 0, 113), // Q + new ScanCodeMapping (16, 81, ConsoleModifiers.Shift, 81), + new ScanCodeMapping (17, 87, 0, 119), // W + new ScanCodeMapping (17, 87, ConsoleModifiers.Shift, 87), + new ScanCodeMapping (18, 69, 0, 101), // E + new ScanCodeMapping (18, 69, ConsoleModifiers.Shift, 69), + new ScanCodeMapping (19, 82, 0, 114), // R + new ScanCodeMapping (19, 82, ConsoleModifiers.Shift, 82), + new ScanCodeMapping (20, 84, 0, 116), // T + new ScanCodeMapping (20, 84, ConsoleModifiers.Shift, 84), + new ScanCodeMapping (21, 89, 0, 121), // Y + new ScanCodeMapping (21, 89, ConsoleModifiers.Shift, 89), + new ScanCodeMapping (22, 85, 0, 117), // U + new ScanCodeMapping (22, 85, ConsoleModifiers.Shift, 85), + new ScanCodeMapping (23, 73, 0, 105), // I + new ScanCodeMapping (23, 73, ConsoleModifiers.Shift, 73), + new ScanCodeMapping (24, 79, 0, 111), // O + new ScanCodeMapping (24, 79, ConsoleModifiers.Shift, 79), + new ScanCodeMapping (25, 80, 0, 112), // P + new ScanCodeMapping (25, 80, ConsoleModifiers.Shift, 80), + new ScanCodeMapping (26, 187, 0, 43), // OemPlus + new ScanCodeMapping (26, 187, ConsoleModifiers.Shift, 42), + new ScanCodeMapping (26, 187, ConsoleModifiers.Alt | ConsoleModifiers.Control, 168), + new ScanCodeMapping (27, 186, 0, 180), // Oem1 + new ScanCodeMapping (27, 186, ConsoleModifiers.Shift, 96), + new ScanCodeMapping (28, 13, 0, 13), // Enter + new ScanCodeMapping (28, 13, ConsoleModifiers.Shift, 13), + new ScanCodeMapping (29, 17, 0, 0), // Control + new ScanCodeMapping (29, 17, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (scanCode: 30, virtualKey: 65, modifiers: 0, unicodeChar: 97), // VK = A, UC = 'a' + new ScanCodeMapping (30, 65, ConsoleModifiers.Shift, 65), // VK = A | Shift, UC = 'A' + new ScanCodeMapping (31, 83, 0, 115), // S + new ScanCodeMapping (31, 83, ConsoleModifiers.Shift, 83), + new ScanCodeMapping (32, 68, 0, 100), // D + new ScanCodeMapping (32, 68, ConsoleModifiers.Shift, 68), + new ScanCodeMapping (33, 70, 0, 102), // F + new ScanCodeMapping (33, 70, ConsoleModifiers.Shift, 70), + new ScanCodeMapping (34, 71, 0, 103), // G + new ScanCodeMapping (34, 71, ConsoleModifiers.Shift, 71), + new ScanCodeMapping (35, 72, 0, 104), // H + new ScanCodeMapping (35, 72, ConsoleModifiers.Shift, 72), + new ScanCodeMapping (36, 74, 0, 106), // J + new ScanCodeMapping (36, 74, ConsoleModifiers.Shift, 74), + new ScanCodeMapping (37, 75, 0, 107), // K + new ScanCodeMapping (37, 75, ConsoleModifiers.Shift, 75), + new ScanCodeMapping (38, 76, 0, 108), // L + new ScanCodeMapping (38, 76, ConsoleModifiers.Shift, 76), + new ScanCodeMapping (39, 192, 0, 231), // Oem3 + new ScanCodeMapping (39, 192, ConsoleModifiers.Shift, 199), + new ScanCodeMapping (40, 222, 0, 186), // Oem7 + new ScanCodeMapping (40, 222, ConsoleModifiers.Shift, 170), + new ScanCodeMapping (41, 220, 0, 92), // Oem5 + new ScanCodeMapping (41, 220, ConsoleModifiers.Shift, 124), + new ScanCodeMapping (42, 16, 0, 0), // LShift + new ScanCodeMapping (42, 16, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (43, 191, 0, 126), // Oem2 + new ScanCodeMapping (43, 191, ConsoleModifiers.Shift, 94), + new ScanCodeMapping (44, 90, 0, 122), // Z + new ScanCodeMapping (44, 90, ConsoleModifiers.Shift, 90), + new ScanCodeMapping (45, 88, 0, 120), // X + new ScanCodeMapping (45, 88, ConsoleModifiers.Shift, 88), + new ScanCodeMapping (46, 67, 0, 99), // C + new ScanCodeMapping (46, 67, ConsoleModifiers.Shift, 67), + new ScanCodeMapping (47, 86, 0, 118), // V + new ScanCodeMapping (47, 86, ConsoleModifiers.Shift, 86), + new ScanCodeMapping (48, 66, 0, 98), // B + new ScanCodeMapping (48, 66, ConsoleModifiers.Shift, 66), + new ScanCodeMapping (49, 78, 0, 110), // N + new ScanCodeMapping (49, 78, ConsoleModifiers.Shift, 78), + new ScanCodeMapping (50, 77, 0, 109), // M + new ScanCodeMapping (50, 77, ConsoleModifiers.Shift, 77), + new ScanCodeMapping (51, 188, 0, 44), // OemComma + new ScanCodeMapping (51, 188, ConsoleModifiers.Shift, 59), + new ScanCodeMapping (52, 190, 0, 46), // OemPeriod + new ScanCodeMapping (52, 190, ConsoleModifiers.Shift, 58), + new ScanCodeMapping (53, 189, 0, 45), // OemMinus + new ScanCodeMapping (53, 189, ConsoleModifiers.Shift, 95), + new ScanCodeMapping (54, 16, 0, 0), // RShift + new ScanCodeMapping (54, 16, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (55, 44, 0, 0), // PrintScreen + new ScanCodeMapping (55, 44, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (56, 18, 0, 0), // Alt + new ScanCodeMapping (56, 18, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (57, 32, 0, 32), // Spacebar + new ScanCodeMapping (57, 32, ConsoleModifiers.Shift, 32), + new ScanCodeMapping (58, 20, 0, 0), // Caps + new ScanCodeMapping (58, 20, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (59, 112, 0, 0), // F1 + new ScanCodeMapping (59, 112, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (60, 113, 0, 0), // F2 + new ScanCodeMapping (60, 113, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (61, 114, 0, 0), // F3 + new ScanCodeMapping (61, 114, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (62, 115, 0, 0), // F4 + new ScanCodeMapping (62, 115, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (63, 116, 0, 0), // F5 + new ScanCodeMapping (63, 116, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (64, 117, 0, 0), // F6 + new ScanCodeMapping (64, 117, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (65, 118, 0, 0), // F7 + new ScanCodeMapping (65, 118, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (66, 119, 0, 0), // F8 + new ScanCodeMapping (66, 119, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (67, 120, 0, 0), // F9 + new ScanCodeMapping (67, 120, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (68, 121, 0, 0), // F10 + new ScanCodeMapping (68, 121, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (69, 144, 0, 0), // Num + new ScanCodeMapping (69, 144, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (70, 145, 0, 0), // Scroll + new ScanCodeMapping (70, 145, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (71, 36, 0, 0), // Home + new ScanCodeMapping (71, 36, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (72, 38, 0, 0), // UpArrow + new ScanCodeMapping (72, 38, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (73, 33, 0, 0), // PageUp + new ScanCodeMapping (73, 33, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (74, 109, 0, 45), // Subtract + new ScanCodeMapping (74, 109, ConsoleModifiers.Shift, 45), + new ScanCodeMapping (75, 37, 0, 0), // LeftArrow + new ScanCodeMapping (75, 37, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (76, 12, 0, 0), // Center + new ScanCodeMapping (76, 12, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (77, 39, 0, 0), // RightArrow + new ScanCodeMapping (77, 39, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (78, 107, 0, 43), // Add + new ScanCodeMapping (78, 107, ConsoleModifiers.Shift, 43), + new ScanCodeMapping (79, 35, 0, 0), // End + new ScanCodeMapping (79, 35, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (80, 40, 0, 0), // DownArrow + new ScanCodeMapping (80, 40, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (81, 34, 0, 0), // PageDown + new ScanCodeMapping (81, 34, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (82, 45, 0, 0), // Insert + new ScanCodeMapping (82, 45, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (83, 46, 0, 0), // Delete + new ScanCodeMapping (83, 46, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (86, 226, 0, 60), // OEM 102 + new ScanCodeMapping (86, 226, ConsoleModifiers.Shift, 62), + new ScanCodeMapping (87, 122, 0, 0), // F11 + new ScanCodeMapping (87, 122, ConsoleModifiers.Shift, 0), + new ScanCodeMapping (88, 123, 0, 0), // F12 + new ScanCodeMapping (88, 123, ConsoleModifiers.Shift, 0) + }; + + /// + /// Decode a that is using . + /// + /// The console key info. + /// The decoded or the . + /// If it's a the may be + /// a or a value. + /// + public static ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) + { + if (consoleKeyInfo.Key != ConsoleKey.Packet) { + return consoleKeyInfo; + } + + return GetConsoleKeyFromKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out _); + } + } +} \ No newline at end of file diff --git a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs index 27b1b7e927..98181f0699 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Terminal.Gui.ConsoleDrivers; using Unix.Terminal; namespace Terminal.Gui; @@ -343,103 +344,82 @@ public override void UpdateScreen () public Curses.Window _window; - static Key MapCursesKey (int cursesKey) + static KeyCode MapCursesKey (int cursesKey) { switch (cursesKey) { - case Curses.KeyF1: return Key.F1; - case Curses.KeyF2: return Key.F2; - case Curses.KeyF3: return Key.F3; - case Curses.KeyF4: return Key.F4; - case Curses.KeyF5: return Key.F5; - case Curses.KeyF6: return Key.F6; - case Curses.KeyF7: return Key.F7; - case Curses.KeyF8: return Key.F8; - case Curses.KeyF9: return Key.F9; - case Curses.KeyF10: return Key.F10; - case Curses.KeyF11: return Key.F11; - case Curses.KeyF12: return Key.F12; - case Curses.KeyUp: return Key.CursorUp; - case Curses.KeyDown: return Key.CursorDown; - case Curses.KeyLeft: return Key.CursorLeft; - case Curses.KeyRight: return Key.CursorRight; - case Curses.KeyHome: return Key.Home; - case Curses.KeyEnd: return Key.End; - case Curses.KeyNPage: return Key.PageDown; - case Curses.KeyPPage: return Key.PageUp; - case Curses.KeyDeleteChar: return Key.DeleteChar; - case Curses.KeyInsertChar: return Key.InsertChar; - case Curses.KeyTab: return Key.Tab; - case Curses.KeyBackTab: return Key.BackTab; - case Curses.KeyBackspace: return Key.Backspace; - case Curses.ShiftKeyUp: return Key.CursorUp | Key.ShiftMask; - case Curses.ShiftKeyDown: return Key.CursorDown | Key.ShiftMask; - case Curses.ShiftKeyLeft: return Key.CursorLeft | Key.ShiftMask; - case Curses.ShiftKeyRight: return Key.CursorRight | Key.ShiftMask; - case Curses.ShiftKeyHome: return Key.Home | Key.ShiftMask; - case Curses.ShiftKeyEnd: return Key.End | Key.ShiftMask; - case Curses.ShiftKeyNPage: return Key.PageDown | Key.ShiftMask; - case Curses.ShiftKeyPPage: return Key.PageUp | Key.ShiftMask; - case Curses.AltKeyUp: return Key.CursorUp | Key.AltMask; - case Curses.AltKeyDown: return Key.CursorDown | Key.AltMask; - case Curses.AltKeyLeft: return Key.CursorLeft | Key.AltMask; - case Curses.AltKeyRight: return Key.CursorRight | Key.AltMask; - case Curses.AltKeyHome: return Key.Home | Key.AltMask; - case Curses.AltKeyEnd: return Key.End | Key.AltMask; - case Curses.AltKeyNPage: return Key.PageDown | Key.AltMask; - case Curses.AltKeyPPage: return Key.PageUp | Key.AltMask; - case Curses.CtrlKeyUp: return Key.CursorUp | Key.CtrlMask; - case Curses.CtrlKeyDown: return Key.CursorDown | Key.CtrlMask; - case Curses.CtrlKeyLeft: return Key.CursorLeft | Key.CtrlMask; - case Curses.CtrlKeyRight: return Key.CursorRight | Key.CtrlMask; - case Curses.CtrlKeyHome: return Key.Home | Key.CtrlMask; - case Curses.CtrlKeyEnd: return Key.End | Key.CtrlMask; - case Curses.CtrlKeyNPage: return Key.PageDown | Key.CtrlMask; - case Curses.CtrlKeyPPage: return Key.PageUp | Key.CtrlMask; - case Curses.ShiftCtrlKeyUp: return Key.CursorUp | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyDown: return Key.CursorDown | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyLeft: return Key.CursorLeft | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyRight: return Key.CursorRight | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyHome: return Key.Home | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyEnd: return Key.End | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyNPage: return Key.PageDown | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftCtrlKeyPPage: return Key.PageUp | Key.ShiftMask | Key.CtrlMask; - case Curses.ShiftAltKeyUp: return Key.CursorUp | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyDown: return Key.CursorDown | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyLeft: return Key.CursorLeft | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyRight: return Key.CursorRight | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyNPage: return Key.PageDown | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyPPage: return Key.PageUp | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyHome: return Key.Home | Key.ShiftMask | Key.AltMask; - case Curses.ShiftAltKeyEnd: return Key.End | Key.ShiftMask | Key.AltMask; - case Curses.AltCtrlKeyNPage: return Key.PageDown | Key.AltMask | Key.CtrlMask; - case Curses.AltCtrlKeyPPage: return Key.PageUp | Key.AltMask | Key.CtrlMask; - case Curses.AltCtrlKeyHome: return Key.Home | Key.AltMask | Key.CtrlMask; - case Curses.AltCtrlKeyEnd: return Key.End | Key.AltMask | Key.CtrlMask; - default: return Key.Unknown; + case Curses.KeyF1: return KeyCode.F1; + case Curses.KeyF2: return KeyCode.F2; + case Curses.KeyF3: return KeyCode.F3; + case Curses.KeyF4: return KeyCode.F4; + case Curses.KeyF5: return KeyCode.F5; + case Curses.KeyF6: return KeyCode.F6; + case Curses.KeyF7: return KeyCode.F7; + case Curses.KeyF8: return KeyCode.F8; + case Curses.KeyF9: return KeyCode.F9; + case Curses.KeyF10: return KeyCode.F10; + case Curses.KeyF11: return KeyCode.F11; + case Curses.KeyF12: return KeyCode.F12; + case Curses.KeyUp: return KeyCode.CursorUp; + case Curses.KeyDown: return KeyCode.CursorDown; + case Curses.KeyLeft: return KeyCode.CursorLeft; + case Curses.KeyRight: return KeyCode.CursorRight; + case Curses.KeyHome: return KeyCode.Home; + case Curses.KeyEnd: return KeyCode.End; + case Curses.KeyNPage: return KeyCode.PageDown; + case Curses.KeyPPage: return KeyCode.PageUp; + case Curses.KeyDeleteChar: return KeyCode.DeleteChar; + case Curses.KeyInsertChar: return KeyCode.InsertChar; + case Curses.KeyTab: return KeyCode.Tab; + case Curses.KeyBackTab: return KeyCode.Tab | KeyCode.ShiftMask; + case Curses.KeyBackspace: return KeyCode.Backspace; + case Curses.ShiftKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask; + case Curses.ShiftKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask; + case Curses.ShiftKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask; + case Curses.ShiftKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask; + case Curses.ShiftKeyHome: return KeyCode.Home | KeyCode.ShiftMask; + case Curses.ShiftKeyEnd: return KeyCode.End | KeyCode.ShiftMask; + case Curses.ShiftKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask; + case Curses.ShiftKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask; + case Curses.AltKeyUp: return KeyCode.CursorUp | KeyCode.AltMask; + case Curses.AltKeyDown: return KeyCode.CursorDown | KeyCode.AltMask; + case Curses.AltKeyLeft: return KeyCode.CursorLeft | KeyCode.AltMask; + case Curses.AltKeyRight: return KeyCode.CursorRight | KeyCode.AltMask; + case Curses.AltKeyHome: return KeyCode.Home | KeyCode.AltMask; + case Curses.AltKeyEnd: return KeyCode.End | KeyCode.AltMask; + case Curses.AltKeyNPage: return KeyCode.PageDown | KeyCode.AltMask; + case Curses.AltKeyPPage: return KeyCode.PageUp | KeyCode.AltMask; + case Curses.CtrlKeyUp: return KeyCode.CursorUp | KeyCode.CtrlMask; + case Curses.CtrlKeyDown: return KeyCode.CursorDown | KeyCode.CtrlMask; + case Curses.CtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.CtrlMask; + case Curses.CtrlKeyRight: return KeyCode.CursorRight | KeyCode.CtrlMask; + case Curses.CtrlKeyHome: return KeyCode.Home | KeyCode.CtrlMask; + case Curses.CtrlKeyEnd: return KeyCode.End | KeyCode.CtrlMask; + case Curses.CtrlKeyNPage: return KeyCode.PageDown | KeyCode.CtrlMask; + case Curses.CtrlKeyPPage: return KeyCode.PageUp | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftCtrlKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.CtrlMask; + case Curses.ShiftAltKeyUp: return KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyDown: return KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyLeft: return KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyRight: return KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyNPage: return KeyCode.PageDown | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyPPage: return KeyCode.PageUp | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyHome: return KeyCode.Home | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.ShiftAltKeyEnd: return KeyCode.End | KeyCode.ShiftMask | KeyCode.AltMask; + case Curses.AltCtrlKeyNPage: return KeyCode.PageDown | KeyCode.AltMask | KeyCode.CtrlMask; + case Curses.AltCtrlKeyPPage: return KeyCode.PageUp | KeyCode.AltMask | KeyCode.CtrlMask; + case Curses.AltCtrlKeyHome: return KeyCode.Home | KeyCode.AltMask | KeyCode.CtrlMask; + case Curses.AltCtrlKeyEnd: return KeyCode.End | KeyCode.AltMask | KeyCode.CtrlMask; + default: return KeyCode.Unknown; } } - KeyModifiers _keyModifiers; - - KeyModifiers MapKeyModifiers (Key key) - { - if (_keyModifiers == null) { - _keyModifiers = new KeyModifiers (); - } - - if (!_keyModifiers.Shift && (key & Key.ShiftMask) != 0) { - _keyModifiers.Shift = true; - } - if (!_keyModifiers.Alt && (key & Key.AltMask) != 0) { - _keyModifiers.Alt = true; - } - if (!_keyModifiers.Ctrl && (key & Key.CtrlMask) != 0) { - _keyModifiers.Ctrl = true; - } - - return _keyModifiers; - } - internal void ProcessInput () { int wch; @@ -448,9 +428,7 @@ internal void ProcessInput () if (code == Curses.ERR) { return; } - - _keyModifiers = new KeyModifiers (); - Key k = Key.Null; + KeyCode k = KeyCode.Null; if (code == Curses.KEY_CODE_YES) { while (code == Curses.KEY_CODE_YES && wch == Curses.KeyResize) { @@ -464,14 +442,14 @@ internal void ProcessInput () int wch2 = wch; while (wch2 == Curses.KeyMouse) { - KeyEvent key = null; + Key kea = null; ConsoleKeyInfo [] cki = new ConsoleKeyInfo [] { - new ConsoleKeyInfo ((char)Key.Esc, 0, false, false, false), + new ConsoleKeyInfo ((char)KeyCode.Esc, 0, false, false, false), new ConsoleKeyInfo ('[', 0, false, false, false), new ConsoleKeyInfo ('<', 0, false, false, false) }; code = 0; - HandleEscSeqResponse (ref code, ref k, ref wch2, ref key, ref cki); + HandleEscSeqResponse (ref code, ref k, ref wch2, ref kea, ref cki); } return; } @@ -479,27 +457,26 @@ internal void ProcessInput () if (wch >= 277 && wch <= 288) { // Shift+(F1 - F12) wch -= 12; - k = Key.ShiftMask | MapCursesKey (wch); + k = KeyCode.ShiftMask | MapCursesKey (wch); } else if (wch >= 289 && wch <= 300) { // Ctrl+(F1 - F12) wch -= 24; - k = Key.CtrlMask | MapCursesKey (wch); + k = KeyCode.CtrlMask | MapCursesKey (wch); } else if (wch >= 301 && wch <= 312) { // Ctrl+Shift+(F1 - F12) wch -= 36; - k = Key.CtrlMask | Key.ShiftMask | MapCursesKey (wch); + k = KeyCode.CtrlMask | KeyCode.ShiftMask | MapCursesKey (wch); } else if (wch >= 313 && wch <= 324) { // Alt+(F1 - F12) wch -= 48; - k = Key.AltMask | MapCursesKey (wch); + k = KeyCode.AltMask | MapCursesKey (wch); } else if (wch >= 325 && wch <= 327) { // Shift+Alt+(F1 - F3) wch -= 60; - k = Key.ShiftMask | Key.AltMask | MapCursesKey (wch); + k = KeyCode.ShiftMask | KeyCode.AltMask | MapCursesKey (wch); } - OnKeyDown (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); + OnKeyDown (new Key (k)); + OnKeyUp (new Key (k)); return; } @@ -510,85 +487,73 @@ internal void ProcessInput () code = Curses.get_wch (out int wch2); if (code == Curses.KEY_CODE_YES) { - k = Key.AltMask | MapCursesKey (wch); + k = KeyCode.AltMask | MapCursesKey (wch); } + Key key = null; if (code == 0) { - KeyEvent key = null; // The ESC-number handling, debatable. // Simulates the AltMask itself by pressing Alt + Space. - if (wch2 == (int)Key.Space) { - k = Key.AltMask; - } else if (wch2 - (int)Key.Space >= (uint)Key.A && wch2 - (int)Key.Space <= (uint)Key.Z) { - k = (Key)((uint)Key.AltMask + (wch2 - (int)Key.Space)); - } else if (wch2 >= (uint)Key.A - 64 && wch2 <= (uint)Key.Z - 64) { - k = (Key)((uint)(Key.AltMask | Key.CtrlMask) + (wch2 + 64)); - } else if (wch2 >= (uint)Key.D0 && wch2 <= (uint)Key.D9) { - k = (Key)((uint)Key.AltMask + (uint)Key.D0 + (wch2 - (uint)Key.D0)); + if (wch2 == (int)KeyCode.Space) { + k = KeyCode.AltMask; + } else if (wch2 - (int)KeyCode.Space >= (uint)KeyCode.A && wch2 - (int)KeyCode.Space <= (uint)KeyCode.Z) { + k = (KeyCode)((uint)KeyCode.AltMask + (wch2 - (int)KeyCode.Space)); + } else if (wch2 >= (uint)KeyCode.A - 64 && wch2 <= (uint)KeyCode.Z - 64) { + k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + (wch2 + 64)); + } else if (wch2 >= (uint)KeyCode.D0 && wch2 <= (uint)KeyCode.D9) { + k = (KeyCode)((uint)KeyCode.AltMask + (uint)KeyCode.D0 + (wch2 - (uint)KeyCode.D0)); } else if (wch2 == Curses.KeyCSI) { ConsoleKeyInfo [] cki = new ConsoleKeyInfo [] { - new ConsoleKeyInfo ((char)Key.Esc, 0, false, false, false), + new ConsoleKeyInfo ((char)KeyCode.Esc, 0, false, false, false), new ConsoleKeyInfo ('[', 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 (((Key)wch2 & Key.CtrlMask) != 0) { - _keyModifiers.Ctrl = true; + if (((KeyCode)wch2 & KeyCode.CtrlMask) != 0) { + k = (KeyCode)((uint)KeyCode.CtrlMask + (wch2 & ~((int)KeyCode.CtrlMask))); } if (wch2 == 0) { - k = Key.CtrlMask | Key.AltMask | Key.Space; - } else if (wch >= (uint)Key.A && wch <= (uint)Key.Z) { - _keyModifiers.Shift = true; - _keyModifiers.Alt = true; + 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 = (Key)wch2; - _keyModifiers.Alt = true; + k = (KeyCode)wch2 | KeyCode.AltMask; } else { - k = (Key)((uint)(Key.AltMask | Key.CtrlMask) + wch2); + k = (KeyCode)((uint)(KeyCode.AltMask | KeyCode.CtrlMask) + wch2); } } - key = new KeyEvent (k, MapKeyModifiers (k)); - OnKeyDown (new KeyEventEventArgs (key)); - OnKeyUp (new KeyEventEventArgs (key)); - OnKeyPressed (new KeyEventEventArgs (key)); + key = new Key (k); } else { - k = Key.Esc; - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); + key = new Key (KeyCode.Esc); } + OnKeyDown (key); + OnKeyUp (key); } else if (wch == Curses.KeyTab) { k = MapCursesKey (wch); - OnKeyDown (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); + OnKeyDown (new Key (k)); + OnKeyUp (new Key (k)); } else { // Unfortunately there are no way to differentiate Ctrl+alfa and Ctrl+Shift+alfa. - k = (Key)wch; + k = (KeyCode)wch; if (wch == 0) { - k = Key.CtrlMask | Key.Space; - } else if (wch >= (uint)Key.A - 64 && wch <= (uint)Key.Z - 64) { - if ((Key)(wch + 64) != Key.J) { - k = Key.CtrlMask | (Key)(wch + 64); + k = KeyCode.CtrlMask | KeyCode.Space; + } else if (wch >= (uint)KeyCode.A - 64 && wch <= (uint)KeyCode.Z - 64) { + if ((KeyCode)(wch + 64) != KeyCode.J) { + k = KeyCode.CtrlMask | (KeyCode)(wch + 64); } - } else if (wch >= (uint)Key.A && wch <= (uint)Key.Z) { - _keyModifiers.Shift = true; - } - OnKeyDown (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (k, MapKeyModifiers (k)))); - } - // Cause OnKeyUp and OnKeyPressed. Note that the special handling for ESC above - // will not impact KeyUp. - // This is causing ESC firing even if another keystroke was handled. - //if (wch == Curses.KeyTab) { - // keyUpHandler (new KeyEvent (MapCursesKey (wch), keyModifiers)); - //} else { - // keyUpHandler (new KeyEvent ((Key)wch, keyModifiers)); - //} + } else if (wch >= (uint)KeyCode.A && wch <= (uint)KeyCode.Z) { + k = (KeyCode)wch | KeyCode.ShiftMask; + } else if (wch <= 'z') { + k = (KeyCode)wch & ~KeyCode.Space; + } + OnKeyDown (new Key (k)); + OnKeyUp (new Key (k)); + } } - void HandleEscSeqResponse (ref int code, ref Key k, ref int wch2, ref KeyEvent key, ref ConsoleKeyInfo [] cki) + void HandleEscSeqResponse (ref int code, ref KeyCode k, ref int wch2, ref Key keyEventArgs, ref ConsoleKeyInfo [] cki) { ConsoleKey ck = 0; ConsoleModifiers mod = 0; @@ -603,15 +568,14 @@ void HandleEscSeqResponse (ref int code, ref Key k, ref int wch2, ref KeyEvent k } cki = null; if (wch2 == 27) { - cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)Key.Esc, 0, + cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)KeyCode.Esc, 0, false, false, false), cki); } } else { k = ConsoleKeyMapping.MapConsoleKeyToKey (consoleKeyInfo.Key, out _); k = ConsoleKeyMapping.MapKeyModifiers (consoleKeyInfo, k); - key = new KeyEvent (k, MapKeyModifiers (k)); - OnKeyDown (new KeyEventEventArgs (key)); - OnKeyPressed (new KeyEventEventArgs (key)); + keyEventArgs = new (k); + OnKeyDown (keyEventArgs); } } else { cki = EscSeqUtils.ResizeArray (consoleKeyInfo, cki); @@ -747,7 +711,7 @@ public override bool EnsureCursorVisibility () public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, bool alt, bool control) { - Key key; + KeyCode key; if (consoleKey == ConsoleKey.Packet) { ConsoleModifiers mod = new ConsoleModifiers (); @@ -760,33 +724,18 @@ public override void SendKeys (char keyChar, ConsoleKey consoleKey, bool shift, if (control) { mod |= ConsoleModifiers.Control; } - var kchar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyChar, mod, out uint ckey, out _); - key = ConsoleKeyMapping.MapConsoleKeyToKey ((ConsoleKey)ckey, out bool mappable); + var cKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey (keyChar, mod, out _); + key = ConsoleKeyMapping.MapConsoleKeyToKey ((ConsoleKey)cKeyInfo.Key, out bool mappable); if (mappable) { - key = (Key)kchar; + key = (KeyCode)cKeyInfo.KeyChar; } } else { - key = (Key)keyChar; + key = (KeyCode)keyChar; } - KeyModifiers km = new KeyModifiers (); - if (shift) { - if (keyChar == 0) { - key |= Key.ShiftMask; - } - km.Shift = shift; - } - if (alt) { - key |= Key.AltMask; - km.Alt = alt; - } - if (control) { - key |= Key.CtrlMask; - km.Ctrl = control; - } - OnKeyDown (new KeyEventEventArgs (new KeyEvent (key, km))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (key, km))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (key, km))); + OnKeyDown (new Key (key)); + OnKeyUp (new Key (key)); + //OnKeyPressed (new KeyEventArgsEventArgs (key)); } diff --git a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs index b90a7a05f6..7468832c15 100644 --- a/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs +++ b/Terminal.Gui/ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs @@ -21,7 +21,7 @@ public static class EscSeqUtils { /// /// Escape key code (ASCII 27/0x1B). /// - public static readonly char KeyEsc = (char)Key.Esc; + public static readonly char KeyEsc = (char)KeyCode.Esc; /// /// ESC [ - The CSI (Control Sequence Introducer). diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs index 69b2032765..4cec5cb019 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Text; +using Terminal.Gui.ConsoleDrivers; using Rune = System.Text.Rune; namespace Terminal.Gui; @@ -461,28 +462,6 @@ public static class FakeConsole { public static bool KeyAvailable { get; } // // Summary: - // Gets a value indicating whether the NUM LOCK keyboard toggle is turned on or - // turned off. - // - // Returns: - // true if NUM LOCK is turned on; false if NUM LOCK is turned off. - /// - /// - /// - public static bool NumberLock { get; } - // - // Summary: - // Gets a value indicating whether the CAPS LOCK keyboard toggle is turned on or - // turned off. - // - // Returns: - // true if CAPS LOCK is turned on; false if CAPS LOCK is turned off. - /// - /// - /// - public static bool CapsLock { get; } - // - // Summary: // Gets a value that indicates whether input has been redirected from the standard // input stream. // @@ -814,17 +793,17 @@ public static int Read () public static Stack MockKeyPresses = new Stack (); /// - /// Helper to push a onto . + /// Helper to push a onto . /// /// - public static void PushMockKeyPress (Key key) + public static void PushMockKeyPress (KeyCode key) { MockKeyPresses.Push (new ConsoleKeyInfo ( - (char)(key & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask), - ConsoleKeyMapping.GetConsoleKeyFromKey (key), - key.HasFlag (Key.ShiftMask), - key.HasFlag (Key.AltMask), - key.HasFlag (Key.CtrlMask))); + (char)(key & ~KeyCode.CtrlMask & ~KeyCode.ShiftMask & ~KeyCode.AltMask), + ConsoleKeyMapping.GetConsoleKeyFromKey (key).Key, + key.HasFlag (KeyCode.ShiftMask), + key.HasFlag (KeyCode.AltMask), + key.HasFlag (KeyCode.CtrlMask))); } // diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs index f42388da82..a2eef29a18 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -5,11 +5,13 @@ using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Terminal.Gui.ConsoleDrivers; // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; namespace Terminal.Gui; + /// /// Implements a mock ConsoleDriver for unit testing /// @@ -17,9 +19,10 @@ public class FakeDriver : ConsoleDriver { #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member public class Behaviors { - public bool UseFakeClipboard { get; internal set; } + public bool FakeClipboardAlwaysThrowsNotSupportedException { get; internal set; } + public bool FakeClipboardIsSupportedAlwaysFalse { get; internal set; } public Behaviors (bool useFakeClipboard = false, bool fakeClipboardAlwaysThrowsNotSupportedException = false, bool fakeClipboardIsSupportedAlwaysTrue = false) @@ -75,9 +78,9 @@ internal override MainLoop Init () ResizeScreen (); CurrentAttribute = new Attribute (Color.White, Color.Black); ClearContents (); - + _mainLoopDriver = new FakeMainLoop (this); - _mainLoopDriver.KeyPressed = ProcessInput; + _mainLoopDriver.MockKeyPressed = MockKeyPressedHandler; return new MainLoop (_mainLoopDriver); } @@ -181,7 +184,6 @@ public override void Refresh () } #region Color Handling - ///// ///// In the FakeDriver, colors are encoded as an int; same as NetDriver ///// However, the foreground color is stored in the most significant 16 bits, @@ -196,62 +198,46 @@ public override void Refresh () // background: background // ); //} - #endregion - public ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) - { - if (consoleKeyInfo.Key != ConsoleKey.Packet) { - return consoleKeyInfo; - } - - var mod = consoleKeyInfo.Modifiers; - var shift = (mod & ConsoleModifiers.Shift) != 0; - var alt = (mod & ConsoleModifiers.Alt) != 0; - var control = (mod & ConsoleModifiers.Control) != 0; - - var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _); - return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control); - } - - Key MapKey (ConsoleKeyInfo keyInfo) + KeyCode MapKey (ConsoleKeyInfo keyInfo) { switch (keyInfo.Key) { case ConsoleKey.Escape: - return MapKeyModifiers (keyInfo, Key.Esc); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Esc); case ConsoleKey.Tab: - return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Tab); case ConsoleKey.Clear: - return MapKeyModifiers (keyInfo, Key.Clear); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Clear); case ConsoleKey.Home: - return MapKeyModifiers (keyInfo, Key.Home); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Home); case ConsoleKey.End: - return MapKeyModifiers (keyInfo, Key.End); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.End); case ConsoleKey.LeftArrow: - return MapKeyModifiers (keyInfo, Key.CursorLeft); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorLeft); case ConsoleKey.RightArrow: - return MapKeyModifiers (keyInfo, Key.CursorRight); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorRight); case ConsoleKey.UpArrow: - return MapKeyModifiers (keyInfo, Key.CursorUp); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorUp); case ConsoleKey.DownArrow: - return MapKeyModifiers (keyInfo, Key.CursorDown); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorDown); case ConsoleKey.PageUp: - return MapKeyModifiers (keyInfo, Key.PageUp); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageUp); case ConsoleKey.PageDown: - return MapKeyModifiers (keyInfo, Key.PageDown); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageDown); case ConsoleKey.Enter: - return MapKeyModifiers (keyInfo, Key.Enter); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Enter); case ConsoleKey.Spacebar: - return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? KeyCode.Space : (KeyCode)keyInfo.KeyChar); case ConsoleKey.Backspace: - return MapKeyModifiers (keyInfo, Key.Backspace); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Backspace); case ConsoleKey.Delete: - return MapKeyModifiers (keyInfo, Key.DeleteChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.DeleteChar); case ConsoleKey.Insert: - return MapKeyModifiers (keyInfo, Key.InsertChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.InsertChar); case ConsoleKey.PrintScreen: - return MapKeyModifiers (keyInfo, Key.PrintScreen); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PrintScreen); case ConsoleKey.Oem1: case ConsoleKey.Oem2: @@ -267,114 +253,42 @@ Key MapKey (ConsoleKeyInfo keyInfo) case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: if (keyInfo.KeyChar == 0) { - return Key.Unknown; + return KeyCode.Unknown; } - return (Key)((uint)keyInfo.KeyChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)keyInfo.KeyChar)); } var key = keyInfo.Key; if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { var delta = key - ConsoleKey.A; - if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.A + delta)); - } - if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); + if (keyInfo.KeyChar != (uint)key) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)keyInfo.KeyChar); } - if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); - } - if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0) { - return (Key)(((uint)Key.AltMask | (uint)Key.CtrlMask) | ((uint)Key.A + delta)); - } else { - return (Key)((uint)keyInfo.KeyChar); - } + if (keyInfo.Modifiers.HasFlag (ConsoleModifiers.Control) + || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt) + || keyInfo.Modifiers.HasFlag (ConsoleModifiers.Shift)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.A + delta)); } - return (Key)((uint)keyInfo.KeyChar); - } - if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { - var delta = key - ConsoleKey.D0; - if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.D0 + delta)); - } - if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); - } - if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); - } - if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); - } - } - return (Key)((uint)keyInfo.KeyChar); - } - if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) { - var delta = key - ConsoleKey.F1; - if ((keyInfo.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.F1 + delta)); - } - - return (Key)((uint)Key.F1 + delta); - } - if (keyInfo.KeyChar != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar)); + var alphaBase = ((keyInfo.Modifiers != ConsoleModifiers.Shift)) ? 'A' : 'a'; + return (KeyCode)((uint)alphaBase + delta); } - return (Key)(0xffffffff); - } - - KeyModifiers keyModifiers; - - private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) - { - Key keyMod = new Key (); - if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) { - keyMod = Key.ShiftMask; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) { - keyMod |= Key.CtrlMask; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) { - keyMod |= Key.AltMask; - } - - return keyMod != Key.Null ? keyMod | key : key; + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)keyInfo.KeyChar)); } private CursorVisibility _savedCursorVisibility; - - void ProcessInput (ConsoleKeyInfo consoleKey) + void MockKeyPressedHandler (ConsoleKeyInfo consoleKeyInfo) { - if (consoleKey.Key == ConsoleKey.Packet) { - consoleKey = FromVKPacketToKConsoleKeyInfo (consoleKey); - } - keyModifiers = new KeyModifiers (); - if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Shift)) { - keyModifiers.Shift = true; - } - if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Alt)) { - keyModifiers.Alt = true; - } - if (consoleKey.Modifiers.HasFlag (ConsoleModifiers.Control)) { - keyModifiers.Ctrl = true; - } - var map = MapKey (consoleKey); - if (map == (Key)0xffffffff) { - if ((consoleKey.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - OnKeyDown(new KeyEventEventArgs(new KeyEvent (map, keyModifiers))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (map, keyModifiers))); - } - return; + if (consoleKeyInfo.Key == ConsoleKey.Packet) { + consoleKeyInfo = ConsoleKeyMapping.FromVKPacketToKConsoleKeyInfo (consoleKeyInfo); } - OnKeyDown (new KeyEventEventArgs (new KeyEvent (map, keyModifiers))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (map, keyModifiers))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (map, keyModifiers))); + var map = MapKey (consoleKeyInfo); + OnKeyDown (new Key (map)); + OnKeyUp (new Key (map)); + //OnKeyPressed (new KeyEventArgs (map)); } /// @@ -410,7 +324,7 @@ public override bool EnsureCursorVisibility () public override void SendKeys (char keyChar, ConsoleKey key, bool shift, bool alt, bool control) { - ProcessInput (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); + MockKeyPressedHandler (new ConsoleKeyInfo (keyChar, key, shift, alt, control)); } public void SetBufferSize (int width, int height) @@ -480,15 +394,14 @@ public override void UpdateCursor () if (Col >= 0 && Col < FakeConsole.BufferWidth && Row >= 0 && Row < FakeConsole.BufferHeight) { FakeConsole.SetCursorPosition (Col, Row); } - } catch (System.IO.IOException) { - } catch (ArgumentOutOfRangeException) { - } + } catch (System.IO.IOException) { } catch (ArgumentOutOfRangeException) { } } #region Not Implemented public override void Suspend () { - throw new NotImplementedException (); + return; + //throw new NotImplementedException (); } #endregion diff --git a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs index 0279a27d9d..4fb09326fc 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeMainLoop.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; internal class FakeMainLoop : IMainLoopDriver { - public Action KeyPressed; + public Action MockKeyPressed; public FakeMainLoop (ConsoleDriver consoleDriver = null) { @@ -31,7 +31,7 @@ public bool EventsPending () public void Iteration () { if (FakeConsole.MockKeyPresses.Count > 0) { - KeyPressed?.Invoke (FakeConsole.MockKeyPresses.Pop ()); + MockKeyPressed?.Invoke (FakeConsole.MockKeyPresses.Pop ()); } } diff --git a/Terminal.Gui/ConsoleDrivers/NetDriver.cs b/Terminal.Gui/ConsoleDrivers/NetDriver.cs index 6b06057b30..5aaff3439c 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -10,6 +10,7 @@ using System.Threading; using System.Threading.Tasks; using System.Text; +using Terminal.Gui.ConsoleDrivers; using static Terminal.Gui.NetEvents; namespace Terminal.Gui; @@ -208,11 +209,11 @@ void ProcessInputQueue () } catch (OperationCanceledException) { return; } - if ((consoleKeyInfo.KeyChar == (char)Key.Esc && !_isEscSeq) - || (consoleKeyInfo.KeyChar != (char)Key.Esc && _isEscSeq)) { + if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq) + || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)) { - if (_cki == null && consoleKeyInfo.KeyChar != (char)Key.Esc && _isEscSeq) { - _cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)Key.Esc, 0, + if (_cki == null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq) { + _cki = EscSeqUtils.ResizeArray (new ConsoleKeyInfo ((char)KeyCode.Esc, 0, false, false, false), _cki); } _isEscSeq = true; @@ -223,7 +224,7 @@ void ProcessInputQueue () _cki = null; _isEscSeq = false; break; - } else if (consoleKeyInfo.KeyChar == (char)Key.Esc && _isEscSeq && _cki != null) { + } else if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki != null) { ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod); _cki = null; if (Console.KeyAvailable) { @@ -822,7 +823,7 @@ public override void UpdateScreen () // output.Append (combMark); //} // WriteToConsole (output, ref lastCol, row, ref outputWidth); - } else if ((rune.IsSurrogatePair () && rune.GetColumns () < 2)) { + } else if ((rune.IsSurrogatePair () && rune.GetColumns () < 2)) { WriteToConsole (output, ref lastCol, row, ref outputWidth); SetCursorPosition (col - 1, row); } @@ -997,45 +998,44 @@ ConsoleKeyInfo FromVKPacketToKConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo) var alt = (mod & ConsoleModifiers.Alt) != 0; var control = (mod & ConsoleModifiers.Control) != 0; - var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out uint virtualKey, out _); + var cKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey (consoleKeyInfo.KeyChar, consoleKeyInfo.Modifiers, out _); - return new ConsoleKeyInfo ((char)keyChar, (ConsoleKey)virtualKey, shift, alt, control); + return new ConsoleKeyInfo (cKeyInfo.KeyChar, cKeyInfo.Key, shift, alt, control); } - Key MapKey (ConsoleKeyInfo keyInfo) + KeyCode MapKey (ConsoleKeyInfo keyInfo) { - MapKeyModifiers (keyInfo, (Key)keyInfo.Key); switch (keyInfo.Key) { case ConsoleKey.Escape: - return MapKeyModifiers (keyInfo, Key.Esc); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Esc); case ConsoleKey.Tab: - return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Tab); case ConsoleKey.Home: - return MapKeyModifiers (keyInfo, Key.Home); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Home); case ConsoleKey.End: - return MapKeyModifiers (keyInfo, Key.End); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.End); case ConsoleKey.LeftArrow: - return MapKeyModifiers (keyInfo, Key.CursorLeft); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorLeft); case ConsoleKey.RightArrow: - return MapKeyModifiers (keyInfo, Key.CursorRight); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorRight); case ConsoleKey.UpArrow: - return MapKeyModifiers (keyInfo, Key.CursorUp); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorUp); case ConsoleKey.DownArrow: - return MapKeyModifiers (keyInfo, Key.CursorDown); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorDown); case ConsoleKey.PageUp: - return MapKeyModifiers (keyInfo, Key.PageUp); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageUp); case ConsoleKey.PageDown: - return MapKeyModifiers (keyInfo, Key.PageDown); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageDown); case ConsoleKey.Enter: - return MapKeyModifiers (keyInfo, Key.Enter); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Enter); case ConsoleKey.Spacebar: - return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? KeyCode.Space : (KeyCode)keyInfo.KeyChar); case ConsoleKey.Backspace: - return MapKeyModifiers (keyInfo, Key.Backspace); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Backspace); case ConsoleKey.Delete: - return MapKeyModifiers (keyInfo, Key.DeleteChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.DeleteChar); case ConsoleKey.Insert: - return MapKeyModifiers (keyInfo, Key.InsertChar); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.InsertChar); case ConsoleKey.Oem1: case ConsoleKey.Oem2: @@ -1046,79 +1046,85 @@ Key MapKey (ConsoleKeyInfo keyInfo) case ConsoleKey.Oem7: case ConsoleKey.Oem8: case ConsoleKey.Oem102: + var ret = ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)keyInfo.KeyChar)); + if (ret.HasFlag (KeyCode.ShiftMask)) { + ret &= ~KeyCode.ShiftMask; + } + return ret; + case ConsoleKey.OemPeriod: case ConsoleKey.OemComma: case ConsoleKey.OemPlus: case ConsoleKey.OemMinus: - return (Key)((uint)keyInfo.KeyChar); + return (KeyCode)((uint)keyInfo.KeyChar); } var key = keyInfo.Key; - if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + if (key is >= ConsoleKey.A and <= ConsoleKey.Z) { var delta = key - ConsoleKey.A; if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.A + delta)); + return (KeyCode)(((uint)KeyCode.CtrlMask) | ((uint)KeyCode.A + delta)); } if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); + return (KeyCode)(((uint)KeyCode.AltMask) | ((uint)KeyCode.A + delta)); + } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.A + delta)); } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.A + delta)); } } - return (Key)((uint)keyInfo.KeyChar); + + if (((keyInfo.Modifiers == ConsoleModifiers.Shift) /*^ (keyInfoEx.CapsLock)*/)) { + if (keyInfo.KeyChar <= (uint)KeyCode.Z) { + return (KeyCode)((uint)KeyCode.A + delta) | KeyCode.ShiftMask; + } + } + // This is buggy because is converting a lower case to a upper case and mustn't + //if (((KeyCode)((uint)keyInfo.KeyChar) & KeyCode.Space) == KeyCode.Space) { + // return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space; + //} + return (KeyCode)(uint)keyInfo.KeyChar; } - if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + if (key is >= ConsoleKey.D0 and <= ConsoleKey.D9) { var delta = key - ConsoleKey.D0; if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.D0 + delta)); + return (KeyCode)(((uint)KeyCode.AltMask) | ((uint)KeyCode.D0 + delta)); } if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); + return (KeyCode)(((uint)KeyCode.CtrlMask) | ((uint)KeyCode.D0 + delta)); } if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); + if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)KeyCode.D0 + delta)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.D0 + delta)); } } - return (Key)((uint)keyInfo.KeyChar); + return (KeyCode)((uint)keyInfo.KeyChar); } if (key is >= ConsoleKey.F1 and <= ConsoleKey.F12) { var delta = key - ConsoleKey.F1; if ((keyInfo.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.F1 + delta)); + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.F1 + delta)); } - return (Key)((uint)Key.F1 + delta); - } - if (keyInfo.KeyChar != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar)); + return (KeyCode)((uint)KeyCode.F1 + delta); } - return (Key)(0xffffffff); - } - - KeyModifiers _keyModifiers; - - Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) - { - _keyModifiers ??= new KeyModifiers (); - Key keyMod = new Key (); - if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) { - keyMod = Key.ShiftMask; - _keyModifiers.Shift = true; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) { - keyMod |= Key.CtrlMask; - _keyModifiers.Ctrl = true; + // Is it a key between a..z? + if ((char)keyInfo.KeyChar is >= 'a' and <= 'z') { + // 'a' should be Key.A + return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space; } - if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) { - keyMod |= Key.AltMask; - _keyModifiers.Alt = true; + + // Is it a key between A..Z? + if (((KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z) { + // It's Key.A...Z. Make it Key.A | Key.ShiftMask + return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space | KeyCode.ShiftMask; } - return keyMod != Key.Null ? keyMod | key : key; + return (KeyCode)(uint)keyInfo.KeyChar; } volatile bool _winSizeChanging; @@ -1131,19 +1137,10 @@ void ProcessInput (NetEvents.InputResult inputEvent) if (consoleKeyInfo.Key == ConsoleKey.Packet) { consoleKeyInfo = FromVKPacketToKConsoleKeyInfo (consoleKeyInfo); } - _keyModifiers = new KeyModifiers (); var map = MapKey (consoleKeyInfo); - if (map == (Key)0xffffffff) { - return; - } - if (map == Key.Null) { - OnKeyDown (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - } else { - OnKeyDown (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - OnKeyUp (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - } + + OnKeyDown (new Key (map)); + OnKeyUp (new Key (map)); break; case NetEvents.EventType.Mouse: OnMouseEvent (new MouseEventEventArgs (ToDriverMouse (inputEvent.MouseEvent))); diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index 3e10564c1f..b78d0d2dc3 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -22,6 +22,8 @@ using System.Threading; using System.Threading.Tasks; using System.Diagnostics; +using Terminal.Gui.ConsoleDrivers; +using static Unix.Terminal.Delegates; namespace Terminal.Gui; @@ -376,6 +378,8 @@ public struct KeyEventRecord { public char UnicodeChar; [FieldOffset (12), MarshalAs (UnmanagedType.U4)] public ControlKeyState dwControlKeyState; + + public override readonly string ToString () => $"[KeyEventRecord({(bKeyDown ? "down" : "up")},{wRepeatCount},{wVirtualKeyCode},{wVirtualScanCode},{new Rune (UnicodeChar).MakePrintable ()},{dwControlKeyState})]"; } [Flags] @@ -595,8 +599,30 @@ public ConsoleKeyInfoEx (ConsoleKeyInfo consoleKeyInfo, bool capslock, bool numl NumLock = numlock; ScrollLock = scrolllock; } + + /// + /// Prints a ConsoleKeyInfoEx structure + /// + /// + /// + public readonly string ToString (ConsoleKeyInfoEx ex) + { + var ke = new Key ((KeyCode)ex.ConsoleKeyInfo.KeyChar); + var sb = new StringBuilder (); + sb.Append ($"Key: {(KeyCode)ex.ConsoleKeyInfo.Key} ({ex.ConsoleKeyInfo.Key})"); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty); + sb.Append ((ex.ConsoleKeyInfo.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty); + sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)ex.ConsoleKeyInfo.KeyChar}) "); + sb.Append ((ex.CapsLock ? "caps," : string.Empty)); + sb.Append ((ex.NumLock ? "num," : string.Empty)); + sb.Append ((ex.ScrollLock ? "scroll," : string.Empty)); + var s = sb.ToString ().TrimEnd (',').TrimEnd (' '); + return $"[ConsoleKeyInfoEx({s})]"; + } } + [DllImport ("kernel32.dll", SetLastError = true)] static extern IntPtr GetStdHandle (int nStdHandle); @@ -875,14 +901,172 @@ private void ChangeWin (Object s, SizeChangedEventArgs e) } #endif - // This is a bit hacky, but it enables users to hold down a key and - // OnKeyDown, OnKeyPressed, OnKeyPressed, OnKeyUp - // It might be worth making OnKeyDown and OnKeyUp virtual so this can be tracked from those calls in case - // somoene calls them externally?? - // - // It also is broken when modifiers keys are down too - // - //Key _keyDown = (Key)0xffffffff; + KeyCode MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) + { + var keyInfo = keyInfoEx.ConsoleKeyInfo; + switch (keyInfo.Key) { + case ConsoleKey.Escape: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Esc); + case ConsoleKey.Tab: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Tab); + case ConsoleKey.Clear: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Clear); + case ConsoleKey.Home: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Home); + case ConsoleKey.End: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.End); + case ConsoleKey.LeftArrow: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorLeft); + case ConsoleKey.RightArrow: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorRight); + case ConsoleKey.UpArrow: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorUp); + case ConsoleKey.DownArrow: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.CursorDown); + case ConsoleKey.PageUp: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageUp); + case ConsoleKey.PageDown: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PageDown); + case ConsoleKey.Enter: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Enter); + case ConsoleKey.Spacebar: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? KeyCode.Space : (KeyCode)keyInfo.KeyChar); + case ConsoleKey.Backspace: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.Backspace); + case ConsoleKey.Delete: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.DeleteChar); + case ConsoleKey.Insert: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.InsertChar); + case ConsoleKey.PrintScreen: + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, KeyCode.PrintScreen); + + //case ConsoleKey.NumPad0: + // return keyInfoEx.NumLock ? Key.D0 : Key.InsertChar; + //case ConsoleKey.NumPad1: + // return keyInfoEx.NumLock ? Key.D1 : Key.End; + //case ConsoleKey.NumPad2: + // return keyInfoEx.NumLock ? Key.D2 : Key.CursorDown; + //case ConsoleKey.NumPad3: + // return keyInfoEx.NumLock ? Key.D3 : Key.PageDown; + //case ConsoleKey.NumPad4: + // return keyInfoEx.NumLock ? Key.D4 : Key.CursorLeft; + //case ConsoleKey.NumPad5: + // return keyInfoEx.NumLock ? Key.D5 : (Key)((uint)keyInfo.KeyChar); + //case ConsoleKey.NumPad6: + // return keyInfoEx.NumLock ? Key.D6 : Key.CursorRight; + //case ConsoleKey.NumPad7: + // return keyInfoEx.NumLock ? Key.D7 : Key.Home; + //case ConsoleKey.NumPad8: + // return keyInfoEx.NumLock ? Key.D8 : Key.CursorUp; + //case ConsoleKey.NumPad9: + // return keyInfoEx.NumLock ? Key.D9 : Key.PageUp; + + case ConsoleKey.Oem1: + case ConsoleKey.Oem2: + case ConsoleKey.Oem3: + case ConsoleKey.Oem4: + case ConsoleKey.Oem5: + case ConsoleKey.Oem6: + case ConsoleKey.Oem7: + case ConsoleKey.Oem8: + case ConsoleKey.Oem102: + var ret = ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)keyInfo.KeyChar)); + if (ret.HasFlag (KeyCode.ShiftMask)) { + ret &= ~KeyCode.ShiftMask; + } + return ret; + + case ConsoleKey.OemPeriod: + case ConsoleKey.OemComma: + case ConsoleKey.OemPlus: + case ConsoleKey.OemMinus: + return (KeyCode)((uint)keyInfo.KeyChar); + } + + var key = keyInfo.Key; + + if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { + var delta = key - ConsoleKey.A; + if (keyInfo.Modifiers == ConsoleModifiers.Control) { + return (KeyCode)(((uint)KeyCode.CtrlMask) | ((uint)KeyCode.A + delta)); + } + if (keyInfo.Modifiers == ConsoleModifiers.Alt) { + return (KeyCode)(((uint)KeyCode.AltMask) | ((uint)KeyCode.A + delta)); + } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.A + delta)); + } + if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.A + delta)); + } + } + + if (((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock))) { + if (keyInfo.KeyChar <= (uint)KeyCode.Z) { + return (KeyCode)((uint)KeyCode.A + delta) | KeyCode.ShiftMask; + } + } + + if (((KeyCode)((uint)keyInfo.KeyChar) & KeyCode.Space) == 0) { + return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space; + } + + if (((KeyCode)((uint)keyInfo.KeyChar) & KeyCode.Space) != 0) { + if (((KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space) == (KeyCode)keyInfo.Key) { + return (KeyCode)((uint)keyInfo.KeyChar) & ~KeyCode.Space; + } + return (KeyCode)((uint)keyInfo.KeyChar); + } + + return (KeyCode)(uint)keyInfo.KeyChar; + + } + + if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { + var delta = key - ConsoleKey.D0; + if (keyInfo.Modifiers == ConsoleModifiers.Alt) { + return (KeyCode)(((uint)KeyCode.AltMask) | ((uint)KeyCode.D0 + delta)); + } + if (keyInfo.Modifiers == ConsoleModifiers.Control) { + return (KeyCode)(((uint)KeyCode.CtrlMask) | ((uint)KeyCode.D0 + delta)); + } + if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.D0 + delta)); + } + if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)KeyCode.D0 + delta)) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.D0 + delta)); + } + } + return (KeyCode)((uint)keyInfo.KeyChar); + } + + if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) { + var delta = key - ConsoleKey.F1; + if ((keyInfo.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)KeyCode.F1 + delta)); + } + + return (KeyCode)((uint)KeyCode.F1 + delta); + } + + if (key == (ConsoleKey)16) { // Shift + return KeyCode.Null | KeyCode.ShiftMask; + } + + if (key == (ConsoleKey)17) { // Ctrl + return KeyCode.Null | KeyCode.CtrlMask; + } + + if (key == (ConsoleKey)18) { // Alt + return KeyCode.Null | KeyCode.AltMask; + } + + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)((uint)keyInfo.KeyChar)); + } + + bool _altDown = false; internal void ProcessInput (WindowsConsole.InputRecord inputEvent) { @@ -892,101 +1076,43 @@ internal void ProcessInput (WindowsConsole.InputRecord inputEvent) if (fromPacketKey) { inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); } - var map = MapKey (ToConsoleKeyInfoEx (inputEvent.KeyEvent)); - //var ke = inputEvent.KeyEvent; - //System.Diagnostics.Debug.WriteLine ($"fromPacketKey: {fromPacketKey}"); - //if (ke.UnicodeChar == '\0') { - // System.Diagnostics.Debug.WriteLine ("UnicodeChar: 0'\\0'"); - //} else if (ke.UnicodeChar == 13) { - // System.Diagnostics.Debug.WriteLine ("UnicodeChar: 13'\\n'"); - //} else { - // System.Diagnostics.Debug.WriteLine ($"UnicodeChar: {(uint)ke.UnicodeChar}'{ke.UnicodeChar}'"); - //} - //System.Diagnostics.Debug.WriteLine ($"bKeyDown: {ke.bKeyDown}"); - //System.Diagnostics.Debug.WriteLine ($"dwControlKeyState: {ke.dwControlKeyState}"); - //System.Diagnostics.Debug.WriteLine ($"wRepeatCount: {ke.wRepeatCount}"); - //System.Diagnostics.Debug.WriteLine ($"wVirtualKeyCode: {ke.wVirtualKeyCode}"); - //System.Diagnostics.Debug.WriteLine ($"wVirtualScanCode: {ke.wVirtualScanCode}"); - - if (map == (Key)0xffffffff) { - KeyEvent key = new KeyEvent (); - - // Shift = VK_SHIFT = 0x10 - // Ctrl = VK_CONTROL = 0x11 - // Alt = VK_MENU = 0x12 - - if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.CapslockOn)) { - inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.CapslockOn; - } + var keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); + Debug.WriteLine ($"event: {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); - if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.ScrolllockOn)) { - inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.ScrolllockOn; - } - if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.NumlockOn)) { - inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.NumlockOn; - } + var map = MapKey (keyInfo); - switch (inputEvent.KeyEvent.dwControlKeyState) { - case WindowsConsole.ControlKeyState.RightAltPressed: - case WindowsConsole.ControlKeyState.RightAltPressed | - WindowsConsole.ControlKeyState.LeftControlPressed | - WindowsConsole.ControlKeyState.EnhancedKey: - case WindowsConsole.ControlKeyState.EnhancedKey: - key = new KeyEvent (Key.CtrlMask | Key.AltMask, _keyModifiers); - break; - case WindowsConsole.ControlKeyState.LeftAltPressed: - key = new KeyEvent (Key.AltMask, _keyModifiers); - break; - case WindowsConsole.ControlKeyState.RightControlPressed: - case WindowsConsole.ControlKeyState.LeftControlPressed: - key = new KeyEvent (Key.CtrlMask, _keyModifiers); - break; - case WindowsConsole.ControlKeyState.ShiftPressed: - key = new KeyEvent (Key.ShiftMask, _keyModifiers); - break; - case WindowsConsole.ControlKeyState.NumlockOn: - break; - case WindowsConsole.ControlKeyState.ScrolllockOn: - break; - case WindowsConsole.ControlKeyState.CapslockOn: - break; - default: - key = inputEvent.KeyEvent.wVirtualKeyCode switch { - 0x10 => new KeyEvent (Key.ShiftMask, _keyModifiers), - 0x11 => new KeyEvent (Key.CtrlMask, _keyModifiers), - 0x12 => new KeyEvent (Key.AltMask, _keyModifiers), - _ => new KeyEvent (Key.Unknown, _keyModifiers) - }; - break; - } - - if (inputEvent.KeyEvent.bKeyDown) { - //_keyDown = key.Key; - OnKeyDown (new KeyEventEventArgs (key)); - } else { - //_keyDown = (Key)0xffffffff; - OnKeyUp (new KeyEventEventArgs (key)); - } + if (inputEvent.KeyEvent.bKeyDown) { + _altDown = keyInfo.ConsoleKeyInfo.Modifiers == ConsoleModifiers.Alt; + // Avoid sending repeat keydowns + OnKeyDown (new Key (map)); } else { - if (inputEvent.KeyEvent.bKeyDown) { - // May occurs using SendKeys - _keyModifiers ??= new KeyModifiers (); - - //if (_keyDown == (Key)0xffffffff) { - // Avoid sending repeat keydowns - // _keyDown = map; - OnKeyDown (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - //} - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); + var keyPressedEventArgs = new Key (map); + + // PROTOTYPE: This logic enables `Alt` key presses (down, up, pressed). + // However, if while the 'Alt' key is down, if another key is pressed and + // released, there will be a keypressed event for that and the + // keypressed event for just `Alt` will be suppressed. + // This allows MenuBar to have `Alt` as a keybinding + if (map != KeyCode.AltMask) { + if (keyInfo.ConsoleKeyInfo.Modifiers.HasFlag (ConsoleModifiers.Alt)) { + if (_altDown) { + _altDown = false; + OnKeyUp (new Key (map)); + } + + } + _altDown = false; + // KeyUp of an Alt-key press. + OnKeyUp (keyPressedEventArgs); } else { - //_keyDown = (Key)0xffffffff; - OnKeyUp (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); + OnKeyUp (keyPressedEventArgs); + if (_altDown) { + _altDown = false; + } } } - if (!inputEvent.KeyEvent.bKeyDown && inputEvent.KeyEvent.dwControlKeyState == 0) { - _keyModifiers = null; - } + break; case WindowsConsole.EventType.Mouse: @@ -1278,8 +1404,6 @@ static MouseFlags SetControlKeyStates (WindowsConsole.MouseEventRecord mouseEven return mouseFlag; } - KeyModifiers _keyModifiers; - public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) { var state = keyEvent.dwControlKeyState; @@ -1287,33 +1411,12 @@ public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEve var shift = (state & WindowsConsole.ControlKeyState.ShiftPressed) != 0; var alt = (state & (WindowsConsole.ControlKeyState.LeftAltPressed | WindowsConsole.ControlKeyState.RightAltPressed)) != 0; var control = (state & (WindowsConsole.ControlKeyState.LeftControlPressed | WindowsConsole.ControlKeyState.RightControlPressed)) != 0; - var capsLock = (state & (WindowsConsole.ControlKeyState.CapslockOn)) != 0; - var numLock = (state & (WindowsConsole.ControlKeyState.NumlockOn)) != 0; - var scrollLock = (state & (WindowsConsole.ControlKeyState.ScrolllockOn)) != 0; + var capslock = (state & WindowsConsole.ControlKeyState.CapslockOn) != 0; + var numlock = (state & WindowsConsole.ControlKeyState.NumlockOn) != 0; + var scrolllock = (state & WindowsConsole.ControlKeyState.ScrolllockOn) != 0; - _keyModifiers ??= new KeyModifiers (); - if (shift) { - _keyModifiers.Shift = true; - } - if (alt) { - _keyModifiers.Alt = true; - } - if (control) { - _keyModifiers.Ctrl = true; - } - if (capsLock) { - _keyModifiers.Capslock = true; - } - if (numLock) { - _keyModifiers.Numlock = true; - } - if (scrollLock) { - _keyModifiers.Scrolllock = true; - } - - var consoleKeyInfo = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); - - return new WindowsConsole.ConsoleKeyInfoEx (consoleKeyInfo, capsLock, numLock, scrollLock); + var cki = new ConsoleKeyInfo (keyEvent.UnicodeChar, (ConsoleKey)keyEvent.wVirtualKeyCode, shift, alt, control); + return new WindowsConsole.ConsoleKeyInfoEx (cki, capslock, numlock, scrolllock); } public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsole.KeyEventRecord keyEvent) @@ -1334,169 +1437,18 @@ public WindowsConsole.KeyEventRecord FromVKPacketToKeyEventRecord (WindowsConsol keyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.RightControlPressed)) { mod |= ConsoleModifiers.Control; } - var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (keyEvent.UnicodeChar, mod, out uint virtualKey, out uint scanCode); + var cKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey (keyEvent.UnicodeChar, mod, out uint scanCode); return new WindowsConsole.KeyEventRecord { - UnicodeChar = (char)keyChar, + UnicodeChar = cKeyInfo.KeyChar, bKeyDown = keyEvent.bKeyDown, dwControlKeyState = keyEvent.dwControlKeyState, wRepeatCount = keyEvent.wRepeatCount, - wVirtualKeyCode = (ushort)virtualKey, + wVirtualKeyCode = (ushort)cKeyInfo.Key, wVirtualScanCode = (ushort)scanCode }; } - public Key MapKey (WindowsConsole.ConsoleKeyInfoEx keyInfoEx) - { - var keyInfo = keyInfoEx.ConsoleKeyInfo; - switch (keyInfo.Key) { - case ConsoleKey.Escape: - return MapKeyModifiers (keyInfo, Key.Esc); - case ConsoleKey.Tab: - return keyInfo.Modifiers == ConsoleModifiers.Shift ? Key.BackTab : Key.Tab; - case ConsoleKey.Clear: - return MapKeyModifiers (keyInfo, Key.Clear); - case ConsoleKey.Home: - return MapKeyModifiers (keyInfo, Key.Home); - case ConsoleKey.End: - return MapKeyModifiers (keyInfo, Key.End); - case ConsoleKey.LeftArrow: - return MapKeyModifiers (keyInfo, Key.CursorLeft); - case ConsoleKey.RightArrow: - return MapKeyModifiers (keyInfo, Key.CursorRight); - case ConsoleKey.UpArrow: - return MapKeyModifiers (keyInfo, Key.CursorUp); - case ConsoleKey.DownArrow: - return MapKeyModifiers (keyInfo, Key.CursorDown); - case ConsoleKey.PageUp: - return MapKeyModifiers (keyInfo, Key.PageUp); - case ConsoleKey.PageDown: - return MapKeyModifiers (keyInfo, Key.PageDown); - case ConsoleKey.Enter: - return MapKeyModifiers (keyInfo, Key.Enter); - case ConsoleKey.Spacebar: - return MapKeyModifiers (keyInfo, keyInfo.KeyChar == 0 ? Key.Space : (Key)keyInfo.KeyChar); - case ConsoleKey.Backspace: - return MapKeyModifiers (keyInfo, Key.Backspace); - case ConsoleKey.Delete: - return MapKeyModifiers (keyInfo, Key.DeleteChar); - case ConsoleKey.Insert: - return MapKeyModifiers (keyInfo, Key.InsertChar); - case ConsoleKey.PrintScreen: - return MapKeyModifiers (keyInfo, Key.PrintScreen); - - case ConsoleKey.NumPad0: - return keyInfoEx.NumLock ? Key.D0 : Key.InsertChar; - case ConsoleKey.NumPad1: - return keyInfoEx.NumLock ? Key.D1 : Key.End; - case ConsoleKey.NumPad2: - return keyInfoEx.NumLock ? Key.D2 : Key.CursorDown; - case ConsoleKey.NumPad3: - return keyInfoEx.NumLock ? Key.D3 : Key.PageDown; - case ConsoleKey.NumPad4: - return keyInfoEx.NumLock ? Key.D4 : Key.CursorLeft; - case ConsoleKey.NumPad5: - return keyInfoEx.NumLock ? Key.D5 : (Key)((uint)keyInfo.KeyChar); - case ConsoleKey.NumPad6: - return keyInfoEx.NumLock ? Key.D6 : Key.CursorRight; - case ConsoleKey.NumPad7: - return keyInfoEx.NumLock ? Key.D7 : Key.Home; - case ConsoleKey.NumPad8: - return keyInfoEx.NumLock ? Key.D8 : Key.CursorUp; - case ConsoleKey.NumPad9: - return keyInfoEx.NumLock ? Key.D9 : Key.PageUp; - - case ConsoleKey.Oem1: - case ConsoleKey.Oem2: - case ConsoleKey.Oem3: - case ConsoleKey.Oem4: - case ConsoleKey.Oem5: - case ConsoleKey.Oem6: - case ConsoleKey.Oem7: - case ConsoleKey.Oem8: - case ConsoleKey.Oem102: - case ConsoleKey.OemPeriod: - case ConsoleKey.OemComma: - case ConsoleKey.OemPlus: - case ConsoleKey.OemMinus: - if (keyInfo.KeyChar == 0) { - return Key.Unknown; - } - - return (Key)((uint)keyInfo.KeyChar); - } - - var key = keyInfo.Key; - //var alphaBase = ((keyInfo.Modifiers == ConsoleModifiers.Shift) ^ (keyInfoEx.CapsLock)) ? 'A' : 'a'; - - if (key >= ConsoleKey.A && key <= ConsoleKey.Z) { - var delta = key - ConsoleKey.A; - if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.A + delta)); - } - if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); - } - if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); - } - if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || (keyInfo.KeyChar != 0 && keyInfo.KeyChar >= 1 && keyInfo.KeyChar <= 26)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.A + delta)); - } - } - //return (Key)((uint)alphaBase + delta); - return (Key)((uint)keyInfo.KeyChar); - } - if (key >= ConsoleKey.D0 && key <= ConsoleKey.D9) { - var delta = key - ConsoleKey.D0; - if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.D0 + delta)); - } - if (keyInfo.Modifiers == ConsoleModifiers.Control) { - return (Key)(((uint)Key.CtrlMask) | ((uint)Key.D0 + delta)); - } - if (keyInfo.Modifiers == (ConsoleModifiers.Shift | ConsoleModifiers.Alt)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); - } - if ((keyInfo.Modifiers & (ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - if (keyInfo.KeyChar == 0 || keyInfo.KeyChar == 30 || keyInfo.KeyChar == ((uint)Key.D0 + delta)) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.D0 + delta)); - } - } - return (Key)((uint)keyInfo.KeyChar); - } - if (key >= ConsoleKey.F1 && key <= ConsoleKey.F12) { - var delta = key - ConsoleKey.F1; - if ((keyInfo.Modifiers & (ConsoleModifiers.Shift | ConsoleModifiers.Alt | ConsoleModifiers.Control)) != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)Key.F1 + delta)); - } - - return (Key)((uint)Key.F1 + delta); - } - if (keyInfo.KeyChar != 0) { - return MapKeyModifiers (keyInfo, (Key)((uint)keyInfo.KeyChar)); - } - - return (Key)(0xffffffff); - } - - private Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) - { - Key keyMod = new Key (); - if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) { - keyMod = Key.ShiftMask; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) { - keyMod |= Key.CtrlMask; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) { - keyMod |= Key.AltMask; - } - - return keyMod != Key.Null ? keyMod | key : key; - } - public override bool IsRuneSupported (Rune rune) { return base.IsRuneSupported (rune) && rune.IsBmp; diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index 5f1fdd1036..080b10d170 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -108,6 +108,7 @@ public int Horizontal { /// Gets whether the specified coordinates lie within the thickness (inside the bounding rectangle but outside of /// the rectangle described by . /// + /// Describes the location and size of the rectangle that contains the thickness. /// /// /// if the specified coordinate is within the thickness; otherwise. diff --git a/Terminal.Gui/Input/Command.cs b/Terminal.Gui/Input/Command.cs index b6ce90623c..ed02dbfe19 100644 --- a/Terminal.Gui/Input/Command.cs +++ b/Terminal.Gui/Input/Command.cs @@ -1,392 +1,419 @@ -// These classes use a keybinding system based on the design implemented in Scintilla.Net which is an MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs - -using System; - -namespace Terminal.Gui { - - /// - /// Actions which can be performed by the application or bound to keys in a control. - /// - public enum Command { - - /// - /// Moves down one item (cell, line, etc...). - /// - LineDown, - - /// - /// Extends the selection down one (cell, line, etc...). - /// - LineDownExtend, - - /// - /// Moves down to the last child node of the branch that holds the current selection. - /// - LineDownToLastBranch, - - /// - /// Scrolls down one (cell, line, etc...) (without changing the selection). - /// - ScrollDown, - - // -------------------------------------------------------------------- - - /// - /// Moves up one (cell, line, etc...). - /// - LineUp, - - /// - /// Extends the selection up one item (cell, line, etc...). - /// - LineUpExtend, - - /// - /// Moves up to the first child node of the branch that holds the current selection. - /// - LineUpToFirstBranch, - - /// - /// Scrolls up one item (cell, line, etc...) (without changing the selection). - /// - ScrollUp, - - /// - /// Moves the selection left one by the minimum increment supported by the e.g. single character, cell, item etc. - /// - Left, - - /// - /// Scrolls one item (cell, character, etc...) to the left - /// - ScrollLeft, - - /// - /// Extends the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc. - /// - LeftExtend, - - /// - /// Moves the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc. - /// - Right, - - /// - /// Scrolls one item (cell, character, etc...) to the right. - /// - ScrollRight, - - /// - /// Extends the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc. - /// - RightExtend, - - /// - /// Moves the caret to the start of the previous word. - /// - WordLeft, - - /// - /// Extends the selection to the start of the previous word. - /// - WordLeftExtend, - - /// - /// Moves the caret to the start of the next word. - /// - WordRight, - - /// - /// Extends the selection to the start of the next word. - /// - WordRightExtend, - - /// - /// Cuts to the clipboard the characters from the current position to the end of the line. - /// - CutToEndLine, - - /// - /// Cuts to the clipboard the characters from the current position to the start of the line. - /// - CutToStartLine, - - /// - /// Deletes the characters forwards. - /// - KillWordForwards, - - /// - /// Deletes the characters backwards. - /// - KillWordBackwards, - - /// - /// Toggles overwrite mode such that newly typed text overwrites the text that is - /// already there (typically associated with the Insert key). - /// - ToggleOverwrite, - - /// - /// Enables overwrite mode such that newly typed text overwrites the text that is - /// already there (typically associated with the Insert key). - /// - EnableOverwrite, - - /// - /// Disables overwrite mode () - /// - DisableOverwrite, - - /// - /// Move one page down. - /// - PageDown, - - /// - /// Move one page page extending the selection to cover revealed objects/characters. - /// - PageDownExtend, - - /// - /// Move one page up. - /// - PageUp, - - /// - /// Move one page up extending the selection to cover revealed objects/characters. - /// - PageUpExtend, - - /// - /// Moves to the top/home. - /// - TopHome, - - /// - /// Extends the selection to the top/home. - /// - TopHomeExtend, - - /// - /// Moves to the bottom/end. - /// - BottomEnd, - - /// - /// Extends the selection to the bottom/end. - /// - BottomEndExtend, - - /// - /// Open the selected item. - /// - OpenSelectedItem, - - /// - /// Toggle the checked state. - /// - ToggleChecked, - - /// - /// Accepts the current state (e.g. selection, button press etc). - /// - Accept, - - /// - /// Toggles the Expanded or collapsed state of a a list or item (with subitems). - /// - ToggleExpandCollapse, - - /// - /// Expands a list or item (with subitems). - /// - Expand, - - /// - /// Recursively Expands all child items and their child items (if any). - /// - ExpandAll, - - /// - /// Collapses a list or item (with subitems). - /// - Collapse, - - /// - /// Recursively collapses a list items of their children (if any). - /// - CollapseAll, - - /// - /// Cancels an action or any temporary states on the control e.g. expanding - /// a combo list. - /// - Cancel, - - /// - /// Unix emulation. - /// - UnixEmulation, - - /// - /// Deletes the character on the right. - /// - DeleteCharRight, - - /// - /// Deletes the character on the left. - /// - DeleteCharLeft, - - /// - /// Selects all objects. - /// - SelectAll, - - /// - /// Deletes all objects. - /// - DeleteAll, - - /// - /// Moves the cursor to the start of line. - /// - StartOfLine, - - /// - /// Extends the selection to the start of line. - /// - StartOfLineExtend, - - /// - /// Moves the cursor to the end of line. - /// - EndOfLine, - - /// - /// Extends the selection to the end of line. - /// - EndOfLineExtend, - - /// - /// Moves the cursor to the top of page. - /// - StartOfPage, - - /// - /// Moves the cursor to the bottom of page. - /// - EndOfPage, - - /// - /// Moves to the left page. - /// - PageLeft, - - /// - /// Moves to the right page. - /// - PageRight, - - /// - /// Moves to the left begin. - /// - LeftHome, - - /// - /// Extends the selection to the left begin. - /// - LeftHomeExtend, - - /// - /// Moves to the right end. - /// - RightEnd, - - /// - /// Extends the selection to the right end. - /// - RightEndExtend, - - /// - /// Undo changes. - /// - Undo, - - /// - /// Redo changes. - /// - Redo, - - /// - /// Copies the current selection. - /// - Copy, - - /// - /// Cuts the current selection. - /// - Cut, - - /// - /// Pastes the current selection. - /// - Paste, - - /// - /// Quit a . - /// - QuitToplevel, - - /// - /// Suspend a application (Only implemented in ). - /// - Suspend, - - /// - /// Moves focus to the next view. - /// - NextView, - - /// - /// Moves focuss to the previous view. - /// - PreviousView, - - /// - /// Moves focus to the next view or Toplevel (case of Overlapped). - /// - NextViewOrTop, - - /// - /// Moves focus to the next previous or Toplevel (case of Overlapped). - /// - PreviousViewOrTop, - - /// - /// Refresh. - /// - Refresh, - - /// - /// Toggles the selection. - /// - ToggleExtend, - - /// - /// Inserts a new item. - /// - NewLine, - - /// - /// Tabs to the next item. - /// - Tab, - - /// - /// Tabs back to the previous item. - /// - BackTab - } +// These classes use a keybinding system based on the design implemented in Scintilla.Net which is an +// MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs + +namespace Terminal.Gui; + +/// +/// Actions which can be performed by the application or bound to keys in a control. +/// +public enum Command { + /// + /// The default command. For this focuses the view. + /// + Default, + + /// + /// Moves down one item (cell, line, etc...). + /// + LineDown, + + /// + /// Extends the selection down one (cell, line, etc...). + /// + LineDownExtend, + + /// + /// Moves down to the last child node of the branch that holds the current selection. + /// + LineDownToLastBranch, + + /// + /// Scrolls down one (cell, line, etc...) (without changing the selection). + /// + ScrollDown, + + // -------------------------------------------------------------------- + + /// + /// Moves up one (cell, line, etc...). + /// + LineUp, + + /// + /// Extends the selection up one item (cell, line, etc...). + /// + LineUpExtend, + + /// + /// Moves up to the first child node of the branch that holds the current selection. + /// + LineUpToFirstBranch, + + /// + /// Scrolls up one item (cell, line, etc...) (without changing the selection). + /// + ScrollUp, + + /// + /// Moves the selection left one by the minimum increment supported by the e.g. single character, cell, item etc. + /// + Left, + + /// + /// Scrolls one item (cell, character, etc...) to the left + /// + ScrollLeft, + + /// + /// Extends the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// + LeftExtend, + + /// + /// Moves the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// + Right, + + /// + /// Scrolls one item (cell, character, etc...) to the right. + /// + ScrollRight, + + /// + /// Extends the selection right one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// + RightExtend, + + /// + /// Moves the caret to the start of the previous word. + /// + WordLeft, + + /// + /// Extends the selection to the start of the previous word. + /// + WordLeftExtend, + + /// + /// Moves the caret to the start of the next word. + /// + WordRight, + + /// + /// Extends the selection to the start of the next word. + /// + WordRightExtend, + + /// + /// Cuts to the clipboard the characters from the current position to the end of the line. + /// + CutToEndLine, + + /// + /// Cuts to the clipboard the characters from the current position to the start of the line. + /// + CutToStartLine, + + /// + /// Deletes the characters forwards. + /// + KillWordForwards, + + /// + /// Deletes the characters backwards. + /// + KillWordBackwards, + + /// + /// Toggles overwrite mode such that newly typed text overwrites the text that is + /// already there (typically associated with the Insert key). + /// + ToggleOverwrite, + + /// + /// Enables overwrite mode such that newly typed text overwrites the text that is + /// already there (typically associated with the Insert key). + /// + EnableOverwrite, + + /// + /// Disables overwrite mode () + /// + DisableOverwrite, + + /// + /// Move one page down. + /// + PageDown, + + /// + /// Move one page page extending the selection to cover revealed objects/characters. + /// + PageDownExtend, + + /// + /// Move one page up. + /// + PageUp, + + /// + /// Move one page up extending the selection to cover revealed objects/characters. + /// + PageUpExtend, + + /// + /// Moves to the top/home. + /// + TopHome, + + /// + /// Extends the selection to the top/home. + /// + TopHomeExtend, + + /// + /// Moves to the bottom/end. + /// + BottomEnd, + + /// + /// Extends the selection to the bottom/end. + /// + BottomEndExtend, + + /// + /// Open the selected item. + /// + OpenSelectedItem, + + /// + /// Toggle the checked state. + /// + ToggleChecked, + + /// + /// Accepts the current state (e.g. selection, button press etc). + /// + Accept, + + /// + /// Toggles the Expanded or collapsed state of a a list or item (with subitems). + /// + ToggleExpandCollapse, + + /// + /// Expands a list or item (with subitems). + /// + Expand, + + /// + /// Recursively Expands all child items and their child items (if any). + /// + ExpandAll, + + /// + /// Collapses a list or item (with subitems). + /// + Collapse, + + /// + /// Recursively collapses a list items of their children (if any). + /// + CollapseAll, + + /// + /// Cancels an action or any temporary states on the control e.g. expanding + /// a combo list. + /// + Cancel, + + /// + /// Unix emulation. + /// + UnixEmulation, + + /// + /// Deletes the character on the right. + /// + DeleteCharRight, + + /// + /// Deletes the character on the left. + /// + DeleteCharLeft, + + /// + /// Selects all objects. + /// + SelectAll, + + /// + /// Deletes all objects. + /// + DeleteAll, + + /// + /// Moves the cursor to the start of line. + /// + StartOfLine, + + /// + /// Extends the selection to the start of line. + /// + StartOfLineExtend, + + /// + /// Moves the cursor to the end of line. + /// + EndOfLine, + + /// + /// Extends the selection to the end of line. + /// + EndOfLineExtend, + + /// + /// Moves the cursor to the top of page. + /// + StartOfPage, + + /// + /// Moves the cursor to the bottom of page. + /// + EndOfPage, + + /// + /// Moves to the left page. + /// + PageLeft, + + /// + /// Moves to the right page. + /// + PageRight, + + /// + /// Moves to the left begin. + /// + LeftHome, + + /// + /// Extends the selection to the left begin. + /// + LeftHomeExtend, + + /// + /// Moves to the right end. + /// + RightEnd, + + /// + /// Extends the selection to the right end. + /// + RightEndExtend, + + /// + /// Undo changes. + /// + Undo, + + /// + /// Redo changes. + /// + Redo, + + /// + /// Copies the current selection. + /// + Copy, + + /// + /// Cuts the current selection. + /// + Cut, + + /// + /// Pastes the current selection. + /// + Paste, + + /// + /// Quit a . + /// + QuitToplevel, + + /// + /// Suspend a application (Only implemented in ). + /// + Suspend, + + /// + /// Moves focus to the next view. + /// + NextView, + + /// + /// Moves focuss to the previous view. + /// + PreviousView, + + /// + /// Moves focus to the next view or Toplevel (case of Overlapped). + /// + NextViewOrTop, + + /// + /// Moves focus to the next previous or Toplevel (case of Overlapped). + /// + PreviousViewOrTop, + + /// + /// Refresh. + /// + Refresh, + + /// + /// Toggles the selection. + /// + ToggleExtend, + + /// + /// Inserts a new item. + /// + NewLine, + + /// + /// Tabs to the next item. + /// + Tab, + + /// + /// Tabs back to the previous item. + /// + BackTab, + + /// + /// Saves the current document. + /// + Save, + + /// + /// Saves the current document with a new name. + /// + SaveAs, + + /// + /// Creates a new document. + /// + New, + + /// + /// Moves selection to an item (e.g. highlighting a different menu item) without necessarily accepting it. + /// + Select, + + /// + /// Shows context about the item (e.g. a context menu). + /// + ShowContextMenu } \ No newline at end of file diff --git a/Terminal.Gui/Input/ConsoleKeyMapping.cs b/Terminal.Gui/Input/ConsoleKeyMapping.cs deleted file mode 100644 index 8902c1bf0a..0000000000 --- a/Terminal.Gui/Input/ConsoleKeyMapping.cs +++ /dev/null @@ -1,560 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Globalization; -using System.Linq; - -namespace Terminal.Gui { - /// - /// Helper class to handle the scan code and virtual key from a . - /// - public static class ConsoleKeyMapping { - private class ScanCodeMapping : IEquatable { - public uint ScanCode; - public uint VirtualKey; - public ConsoleModifiers Modifiers; - public uint UnicodeChar; - - public ScanCodeMapping (uint scanCode, uint virtualKey, ConsoleModifiers modifiers, uint unicodeChar) - { - ScanCode = scanCode; - VirtualKey = virtualKey; - Modifiers = modifiers; - UnicodeChar = unicodeChar; - } - - public bool Equals (ScanCodeMapping other) - { - return (this.ScanCode.Equals (other.ScanCode) && - this.VirtualKey.Equals (other.VirtualKey) && - this.Modifiers.Equals (other.Modifiers) && - this.UnicodeChar.Equals (other.UnicodeChar)); - } - } - - private static ConsoleModifiers GetModifiers (uint unicodeChar, ConsoleModifiers modifiers, bool isConsoleKey) - { - if (modifiers.HasFlag (ConsoleModifiers.Shift) && - !modifiers.HasFlag (ConsoleModifiers.Alt) && - !modifiers.HasFlag (ConsoleModifiers.Control)) { - - return ConsoleModifiers.Shift; - } else if (modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { - return modifiers; - } else if ((!isConsoleKey || (isConsoleKey && (modifiers.HasFlag (ConsoleModifiers.Shift) || - modifiers.HasFlag (ConsoleModifiers.Alt) || modifiers.HasFlag (ConsoleModifiers.Control)))) && - unicodeChar >= 65 && unicodeChar <= 90) { - - return ConsoleModifiers.Shift; - } - return 0; - } - - private static ScanCodeMapping GetScanCode (string propName, uint keyValue, ConsoleModifiers modifiers) - { - switch (propName) { - case "UnicodeChar": - var sCode = scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == modifiers); - if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { - return scanCodes.FirstOrDefault ((e) => e.UnicodeChar == keyValue && e.Modifiers == 0); - } - return sCode; - case "VirtualKey": - sCode = scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == modifiers); - if (sCode == null && modifiers == (ConsoleModifiers.Alt | ConsoleModifiers.Control)) { - return scanCodes.FirstOrDefault ((e) => e.VirtualKey == keyValue && e.Modifiers == 0); - } - return sCode; - } - - return null; - } - - /// - /// Gets the from the provided . - /// - /// - /// - public static ConsoleKey GetConsoleKeyFromKey (Key key) - { - ConsoleModifiers mod = new ConsoleModifiers (); - if (key.HasFlag (Key.ShiftMask)) { - mod |= ConsoleModifiers.Shift; - } - if (key.HasFlag (Key.AltMask)) { - mod |= ConsoleModifiers.Alt; - } - if (key.HasFlag (Key.CtrlMask)) { - mod |= ConsoleModifiers.Control; - } - return (ConsoleKey)ConsoleKeyMapping.GetConsoleKeyFromKey ((uint)(key & ~Key.CtrlMask & ~Key.ShiftMask & ~Key.AltMask), mod, out _, out _); - } - - /// - /// Get the from a . - /// - /// The key value. - /// The modifiers keys. - /// The resulting scan code. - /// The resulting output character. - /// The or the . - public static uint GetConsoleKeyFromKey (uint keyValue, ConsoleModifiers modifiers, out uint scanCode, out uint outputChar) - { - scanCode = 0; - outputChar = keyValue; - if (keyValue == 0) { - return 0; - } - - uint consoleKey = MapKeyToConsoleKey (keyValue, out bool mappable); - if (mappable) { - var mod = GetModifiers (keyValue, modifiers, false); - var scode = GetScanCode ("UnicodeChar", keyValue, mod); - if (scode != null) { - consoleKey = scode.VirtualKey; - scanCode = scode.ScanCode; - outputChar = scode.UnicodeChar; - } else { - consoleKey = consoleKey < 0xff ? (uint)(consoleKey & 0xff | 0xff << 8) : consoleKey; - } - } else { - var mod = GetModifiers (keyValue, modifiers, false); - var scode = GetScanCode ("VirtualKey", consoleKey, mod); - if (scode != null) { - consoleKey = scode.VirtualKey; - scanCode = scode.ScanCode; - outputChar = scode.UnicodeChar; - } - } - - return consoleKey; - } - - /// - /// Get the output character from the . - /// - /// The unicode character. - /// The modifiers keys. - /// The resulting console key. - /// The resulting scan code. - /// The output character or the . - public static uint GetKeyCharFromConsoleKey (uint unicodeChar, ConsoleModifiers modifiers, out uint consoleKey, out uint scanCode) - { - uint decodedChar = unicodeChar >> 8 == 0xff ? unicodeChar & 0xff : unicodeChar; - uint keyChar = decodedChar; - consoleKey = 0; - var mod = GetModifiers (decodedChar, modifiers, true); - scanCode = 0; - var scode = unicodeChar != 0 && unicodeChar >> 8 != 0xff ? GetScanCode ("VirtualKey", decodedChar, mod) : null; - if (scode != null) { - consoleKey = scode.VirtualKey; - keyChar = scode.UnicodeChar; - scanCode = scode.ScanCode; - } - if (scode == null) { - scode = unicodeChar != 0 ? GetScanCode ("UnicodeChar", decodedChar, mod) : null; - if (scode != null) { - consoleKey = scode.VirtualKey; - keyChar = scode.UnicodeChar; - scanCode = scode.ScanCode; - } - } - if (decodedChar != 0 && scanCode == 0 && char.IsLetter ((char)decodedChar)) { - string stFormD = ((char)decodedChar).ToString ().Normalize (System.Text.NormalizationForm.FormD); - for (int i = 0; i < stFormD.Length; i++) { - UnicodeCategory uc = CharUnicodeInfo.GetUnicodeCategory (stFormD [i]); - if (uc != UnicodeCategory.NonSpacingMark && uc != UnicodeCategory.OtherLetter) { - consoleKey = char.ToUpper (stFormD [i]); - scode = GetScanCode ("VirtualKey", char.ToUpper (stFormD [i]), 0); - if (scode != null) { - scanCode = scode.ScanCode; - } - } - } - } - - return keyChar; - } - - /// - /// Maps a to a . - /// - /// The key value. - /// If is mapped to a valid character, otherwise . - /// The or the . - public static uint MapKeyToConsoleKey (uint keyValue, out bool isMappable) - { - isMappable = false; - - switch ((Key)keyValue) { - case Key.Delete: - return (uint)ConsoleKey.Delete; - case Key.CursorUp: - return (uint)ConsoleKey.UpArrow; - case Key.CursorDown: - return (uint)ConsoleKey.DownArrow; - case Key.CursorLeft: - return (uint)ConsoleKey.LeftArrow; - case Key.CursorRight: - return (uint)ConsoleKey.RightArrow; - case Key.PageUp: - return (uint)ConsoleKey.PageUp; - case Key.PageDown: - return (uint)ConsoleKey.PageDown; - case Key.Home: - return (uint)ConsoleKey.Home; - case Key.End: - return (uint)ConsoleKey.End; - case Key.InsertChar: - return (uint)ConsoleKey.Insert; - case Key.DeleteChar: - return (uint)ConsoleKey.Delete; - case Key.F1: - return (uint)ConsoleKey.F1; - case Key.F2: - return (uint)ConsoleKey.F2; - case Key.F3: - return (uint)ConsoleKey.F3; - case Key.F4: - return (uint)ConsoleKey.F4; - case Key.F5: - return (uint)ConsoleKey.F5; - case Key.F6: - return (uint)ConsoleKey.F6; - case Key.F7: - return (uint)ConsoleKey.F7; - case Key.F8: - return (uint)ConsoleKey.F8; - case Key.F9: - return (uint)ConsoleKey.F9; - case Key.F10: - return (uint)ConsoleKey.F10; - case Key.F11: - return (uint)ConsoleKey.F11; - case Key.F12: - return (uint)ConsoleKey.F12; - case Key.F13: - return (uint)ConsoleKey.F13; - case Key.F14: - return (uint)ConsoleKey.F14; - case Key.F15: - return (uint)ConsoleKey.F15; - case Key.F16: - return (uint)ConsoleKey.F16; - case Key.F17: - return (uint)ConsoleKey.F17; - case Key.F18: - return (uint)ConsoleKey.F18; - case Key.F19: - return (uint)ConsoleKey.F19; - case Key.F20: - return (uint)ConsoleKey.F20; - case Key.F21: - return (uint)ConsoleKey.F21; - case Key.F22: - return (uint)ConsoleKey.F22; - case Key.F23: - return (uint)ConsoleKey.F23; - case Key.F24: - return (uint)ConsoleKey.F24; - case Key.BackTab: - return (uint)ConsoleKey.Tab; - case Key.Unknown: - isMappable = true; - return 0; - } - isMappable = true; - - return keyValue; - } - - /// - /// Maps a to a . - /// - /// The console key. - /// If is mapped to a valid character, otherwise . - /// The or the . - public static Key MapConsoleKeyToKey (ConsoleKey consoleKey, out bool isMappable) - { - isMappable = false; - - switch (consoleKey) { - case ConsoleKey.Delete: - return Key.Delete; - case ConsoleKey.UpArrow: - return Key.CursorUp; - case ConsoleKey.DownArrow: - return Key.CursorDown; - case ConsoleKey.LeftArrow: - return Key.CursorLeft; - case ConsoleKey.RightArrow: - return Key.CursorRight; - case ConsoleKey.PageUp: - return Key.PageUp; - case ConsoleKey.PageDown: - return Key.PageDown; - case ConsoleKey.Home: - return Key.Home; - case ConsoleKey.End: - return Key.End; - case ConsoleKey.Insert: - return Key.InsertChar; - case ConsoleKey.F1: - return Key.F1; - case ConsoleKey.F2: - return Key.F2; - case ConsoleKey.F3: - return Key.F3; - case ConsoleKey.F4: - return Key.F4; - case ConsoleKey.F5: - return Key.F5; - case ConsoleKey.F6: - return Key.F6; - case ConsoleKey.F7: - return Key.F7; - case ConsoleKey.F8: - return Key.F8; - case ConsoleKey.F9: - return Key.F9; - case ConsoleKey.F10: - return Key.F10; - case ConsoleKey.F11: - return Key.F11; - case ConsoleKey.F12: - return Key.F12; - case ConsoleKey.F13: - return Key.F13; - case ConsoleKey.F14: - return Key.F14; - case ConsoleKey.F15: - return Key.F15; - case ConsoleKey.F16: - return Key.F16; - case ConsoleKey.F17: - return Key.F17; - case ConsoleKey.F18: - return Key.F18; - case ConsoleKey.F19: - return Key.F19; - case ConsoleKey.F20: - return Key.F20; - case ConsoleKey.F21: - return Key.F21; - case ConsoleKey.F22: - return Key.F22; - case ConsoleKey.F23: - return Key.F23; - case ConsoleKey.F24: - return Key.F24; - case ConsoleKey.Tab: - return Key.BackTab; - } - isMappable = true; - - return (Key)consoleKey; - } - - /// - /// Maps a to a . - /// - /// The console key info. - /// The key. - /// The with or the - public static Key MapKeyModifiers (ConsoleKeyInfo keyInfo, Key key) - { - Key keyMod = new Key (); - if ((keyInfo.Modifiers & ConsoleModifiers.Shift) != 0) - keyMod = Key.ShiftMask; - if ((keyInfo.Modifiers & ConsoleModifiers.Control) != 0) - keyMod |= Key.CtrlMask; - if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) - keyMod |= Key.AltMask; - - return keyMod != Key.Null ? keyMod | key : key; - } - - private static HashSet scanCodes = new HashSet { - new ScanCodeMapping (1,27,0,27), // Escape - new ScanCodeMapping (1,27,ConsoleModifiers.Shift,27), - new ScanCodeMapping (2,49,0,49), // D1 - new ScanCodeMapping (2,49,ConsoleModifiers.Shift,33), - new ScanCodeMapping (3,50,0,50), // D2 - new ScanCodeMapping (3,50,ConsoleModifiers.Shift,34), - new ScanCodeMapping (3,50,ConsoleModifiers.Alt | ConsoleModifiers.Control,64), - new ScanCodeMapping (4,51,0,51), // D3 - new ScanCodeMapping (4,51,ConsoleModifiers.Shift,35), - new ScanCodeMapping (4,51,ConsoleModifiers.Alt | ConsoleModifiers.Control,163), - new ScanCodeMapping (5,52,0,52), // D4 - new ScanCodeMapping (5,52,ConsoleModifiers.Shift,36), - new ScanCodeMapping (5,52,ConsoleModifiers.Alt | ConsoleModifiers.Control,167), - new ScanCodeMapping (6,53,0,53), // D5 - new ScanCodeMapping (6,53,ConsoleModifiers.Shift,37), - new ScanCodeMapping (6,53,ConsoleModifiers.Alt | ConsoleModifiers.Control,8364), - new ScanCodeMapping (7,54,0,54), // D6 - new ScanCodeMapping (7,54,ConsoleModifiers.Shift,38), - new ScanCodeMapping (8,55,0,55), // D7 - new ScanCodeMapping (8,55,ConsoleModifiers.Shift,47), - new ScanCodeMapping (8,55,ConsoleModifiers.Alt | ConsoleModifiers.Control,123), - new ScanCodeMapping (9,56,0,56), // D8 - new ScanCodeMapping (9,56,ConsoleModifiers.Shift,40), - new ScanCodeMapping (9,56,ConsoleModifiers.Alt | ConsoleModifiers.Control,91), - new ScanCodeMapping (10,57,0,57), // D9 - new ScanCodeMapping (10,57,ConsoleModifiers.Shift,41), - new ScanCodeMapping (10,57,ConsoleModifiers.Alt | ConsoleModifiers.Control,93), - new ScanCodeMapping (11,48,0,48), // D0 - new ScanCodeMapping (11,48,ConsoleModifiers.Shift,61), - new ScanCodeMapping (11,48,ConsoleModifiers.Alt | ConsoleModifiers.Control,125), - new ScanCodeMapping (12,219,0,39), // Oem4 - new ScanCodeMapping (12,219,ConsoleModifiers.Shift,63), - new ScanCodeMapping (13,221,0,171), // Oem6 - new ScanCodeMapping (13,221,ConsoleModifiers.Shift,187), - new ScanCodeMapping (14,8,0,8), // Backspace - new ScanCodeMapping (14,8,ConsoleModifiers.Shift,8), - new ScanCodeMapping (15,9,0,9), // Tab - new ScanCodeMapping (15,9,ConsoleModifiers.Shift,15), - new ScanCodeMapping (16,81,0,113), // Q - new ScanCodeMapping (16,81,ConsoleModifiers.Shift,81), - new ScanCodeMapping (17,87,0,119), // W - new ScanCodeMapping (17,87,ConsoleModifiers.Shift,87), - new ScanCodeMapping (18,69,0,101), // E - new ScanCodeMapping (18,69,ConsoleModifiers.Shift,69), - new ScanCodeMapping (19,82,0,114), // R - new ScanCodeMapping (19,82,ConsoleModifiers.Shift,82), - new ScanCodeMapping (20,84,0,116), // T - new ScanCodeMapping (20,84,ConsoleModifiers.Shift,84), - new ScanCodeMapping (21,89,0,121), // Y - new ScanCodeMapping (21,89,ConsoleModifiers.Shift,89), - new ScanCodeMapping (22,85,0,117), // U - new ScanCodeMapping (22,85,ConsoleModifiers.Shift,85), - new ScanCodeMapping (23,73,0,105), // I - new ScanCodeMapping (23,73,ConsoleModifiers.Shift,73), - new ScanCodeMapping (24,79,0,111), // O - new ScanCodeMapping (24,79,ConsoleModifiers.Shift,79), - new ScanCodeMapping (25,80,0,112), // P - new ScanCodeMapping (25,80,ConsoleModifiers.Shift,80), - new ScanCodeMapping (26,187,0,43), // OemPlus - new ScanCodeMapping (26,187,ConsoleModifiers.Shift,42), - new ScanCodeMapping (26,187,ConsoleModifiers.Alt | ConsoleModifiers.Control,168), - new ScanCodeMapping (27,186,0,180), // Oem1 - new ScanCodeMapping (27,186,ConsoleModifiers.Shift,96), - new ScanCodeMapping (28,13,0,13), // Enter - new ScanCodeMapping (28,13,ConsoleModifiers.Shift,13), - new ScanCodeMapping (29,17,0,0), // Control - new ScanCodeMapping (29,17,ConsoleModifiers.Shift,0), - new ScanCodeMapping (30,65,0,97), // A - new ScanCodeMapping (30,65,ConsoleModifiers.Shift,65), - new ScanCodeMapping (31,83,0,115), // S - new ScanCodeMapping (31,83,ConsoleModifiers.Shift,83), - new ScanCodeMapping (32,68,0,100), // D - new ScanCodeMapping (32,68,ConsoleModifiers.Shift,68), - new ScanCodeMapping (33,70,0,102), // F - new ScanCodeMapping (33,70,ConsoleModifiers.Shift,70), - new ScanCodeMapping (34,71,0,103), // G - new ScanCodeMapping (34,71,ConsoleModifiers.Shift,71), - new ScanCodeMapping (35,72,0,104), // H - new ScanCodeMapping (35,72,ConsoleModifiers.Shift,72), - new ScanCodeMapping (36,74,0,106), // J - new ScanCodeMapping (36,74,ConsoleModifiers.Shift,74), - new ScanCodeMapping (37,75,0,107), // K - new ScanCodeMapping (37,75,ConsoleModifiers.Shift,75), - new ScanCodeMapping (38,76,0,108), // L - new ScanCodeMapping (38,76,ConsoleModifiers.Shift,76), - new ScanCodeMapping (39,192,0,231), // Oem3 - new ScanCodeMapping (39,192,ConsoleModifiers.Shift,199), - new ScanCodeMapping (40,222,0,186), // Oem7 - new ScanCodeMapping (40,222,ConsoleModifiers.Shift,170), - new ScanCodeMapping (41,220,0,92), // Oem5 - new ScanCodeMapping (41,220,ConsoleModifiers.Shift,124), - new ScanCodeMapping (42,16,0,0), // LShift - new ScanCodeMapping (42,16,ConsoleModifiers.Shift,0), - new ScanCodeMapping (43,191,0,126), // Oem2 - new ScanCodeMapping (43,191,ConsoleModifiers.Shift,94), - new ScanCodeMapping (44,90,0,122), // Z - new ScanCodeMapping (44,90,ConsoleModifiers.Shift,90), - new ScanCodeMapping (45,88,0,120), // X - new ScanCodeMapping (45,88,ConsoleModifiers.Shift,88), - new ScanCodeMapping (46,67,0,99), // C - new ScanCodeMapping (46,67,ConsoleModifiers.Shift,67), - new ScanCodeMapping (47,86,0,118), // V - new ScanCodeMapping (47,86,ConsoleModifiers.Shift,86), - new ScanCodeMapping (48,66,0,98), // B - new ScanCodeMapping (48,66,ConsoleModifiers.Shift,66), - new ScanCodeMapping (49,78,0,110), // N - new ScanCodeMapping (49,78,ConsoleModifiers.Shift,78), - new ScanCodeMapping (50,77,0,109), // M - new ScanCodeMapping (50,77,ConsoleModifiers.Shift,77), - new ScanCodeMapping (51,188,0,44), // OemComma - new ScanCodeMapping (51,188,ConsoleModifiers.Shift,59), - new ScanCodeMapping (52,190,0,46), // OemPeriod - new ScanCodeMapping (52,190,ConsoleModifiers.Shift,58), - new ScanCodeMapping (53,189,0,45), // OemMinus - new ScanCodeMapping (53,189,ConsoleModifiers.Shift,95), - new ScanCodeMapping (54,16,0,0), // RShift - new ScanCodeMapping (54,16,ConsoleModifiers.Shift,0), - new ScanCodeMapping (55,44,0,0), // PrintScreen - new ScanCodeMapping (55,44,ConsoleModifiers.Shift,0), - new ScanCodeMapping (56,18,0,0), // Alt - new ScanCodeMapping (56,18,ConsoleModifiers.Shift,0), - new ScanCodeMapping (57,32,0,32), // Spacebar - new ScanCodeMapping (57,32,ConsoleModifiers.Shift,32), - new ScanCodeMapping (58,20,0,0), // Caps - new ScanCodeMapping (58,20,ConsoleModifiers.Shift,0), - new ScanCodeMapping (59,112,0,0), // F1 - new ScanCodeMapping (59,112,ConsoleModifiers.Shift,0), - new ScanCodeMapping (60,113,0,0), // F2 - new ScanCodeMapping (60,113,ConsoleModifiers.Shift,0), - new ScanCodeMapping (61,114,0,0), // F3 - new ScanCodeMapping (61,114,ConsoleModifiers.Shift,0), - new ScanCodeMapping (62,115,0,0), // F4 - new ScanCodeMapping (62,115,ConsoleModifiers.Shift,0), - new ScanCodeMapping (63,116,0,0), // F5 - new ScanCodeMapping (63,116,ConsoleModifiers.Shift,0), - new ScanCodeMapping (64,117,0,0), // F6 - new ScanCodeMapping (64,117,ConsoleModifiers.Shift,0), - new ScanCodeMapping (65,118,0,0), // F7 - new ScanCodeMapping (65,118,ConsoleModifiers.Shift,0), - new ScanCodeMapping (66,119,0,0), // F8 - new ScanCodeMapping (66,119,ConsoleModifiers.Shift,0), - new ScanCodeMapping (67,120,0,0), // F9 - new ScanCodeMapping (67,120,ConsoleModifiers.Shift,0), - new ScanCodeMapping (68,121,0,0), // F10 - new ScanCodeMapping (68,121,ConsoleModifiers.Shift,0), - new ScanCodeMapping (69,144,0,0), // Num - new ScanCodeMapping (69,144,ConsoleModifiers.Shift,0), - new ScanCodeMapping (70,145,0,0), // Scroll - new ScanCodeMapping (70,145,ConsoleModifiers.Shift,0), - new ScanCodeMapping (71,36,0,0), // Home - new ScanCodeMapping (71,36,ConsoleModifiers.Shift,0), - new ScanCodeMapping (72,38,0,0), // UpArrow - new ScanCodeMapping (72,38,ConsoleModifiers.Shift,0), - new ScanCodeMapping (73,33,0,0), // PageUp - new ScanCodeMapping (73,33,ConsoleModifiers.Shift,0), - new ScanCodeMapping (74,109,0,45), // Subtract - new ScanCodeMapping (74,109,ConsoleModifiers.Shift,45), - new ScanCodeMapping (75,37,0,0), // LeftArrow - new ScanCodeMapping (75,37,ConsoleModifiers.Shift,0), - new ScanCodeMapping (76,12,0,0), // Center - new ScanCodeMapping (76,12,ConsoleModifiers.Shift,0), - new ScanCodeMapping (77,39,0,0), // RightArrow - new ScanCodeMapping (77,39,ConsoleModifiers.Shift,0), - new ScanCodeMapping (78,107,0,43), // Add - new ScanCodeMapping (78,107,ConsoleModifiers.Shift,43), - new ScanCodeMapping (79,35,0,0), // End - new ScanCodeMapping (79,35,ConsoleModifiers.Shift,0), - new ScanCodeMapping (80,40,0,0), // DownArrow - new ScanCodeMapping (80,40,ConsoleModifiers.Shift,0), - new ScanCodeMapping (81,34,0,0), // PageDown - new ScanCodeMapping (81,34,ConsoleModifiers.Shift,0), - new ScanCodeMapping (82,45,0,0), // Insert - new ScanCodeMapping (82,45,ConsoleModifiers.Shift,0), - new ScanCodeMapping (83,46,0,0), // Delete - new ScanCodeMapping (83,46,ConsoleModifiers.Shift,0), - new ScanCodeMapping (86,226,0,60), // OEM 102 - new ScanCodeMapping (86,226,ConsoleModifiers.Shift,62), - new ScanCodeMapping (87,122,0,0), // F11 - new ScanCodeMapping (87,122,ConsoleModifiers.Shift,0), - new ScanCodeMapping (88,123,0,0), // F12 - new ScanCodeMapping (88,123,ConsoleModifiers.Shift,0) - }; - } -} diff --git a/Terminal.Gui/Input/Event.cs b/Terminal.Gui/Input/Event.cs deleted file mode 100644 index 8a50c86757..0000000000 --- a/Terminal.Gui/Input/Event.cs +++ /dev/null @@ -1,837 +0,0 @@ -// -// Evemts.cs: Events, Key mappings -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -using System; - -namespace Terminal.Gui { - - /// - /// Identifies the state of the "shift"-keys within a event. - /// - public class KeyModifiers { - /// - /// Check if the Shift key was pressed or not. - /// - public bool Shift; - /// - /// Check if the Alt key was pressed or not. - /// - public bool Alt; - /// - /// Check if the Ctrl key was pressed or not. - /// - public bool Ctrl; - /// - /// Check if the Caps lock key was pressed or not. - /// - public bool Capslock; - /// - /// Check if the Num lock key was pressed or not. - /// - public bool Numlock; - /// - /// Check if the Scroll lock key was pressed or not. - /// - public bool Scrolllock; - } - - /// - /// The enumeration contains special encoding for some keys, but can also - /// encode all the unicode values that can be passed. - /// - /// - /// - /// If the is set, then the value is that of the special mask, - /// otherwise, the value is the one of the lower bits (as extracted by ) - /// - /// Numerics keys are the values between 48 and 57 corresponding to 0 to 9 - /// - /// - /// - /// Upper alpha keys are the values between 65 and 90 corresponding to A to Z - /// - /// - /// Unicode runes are also stored here, the letter 'A" for example is encoded as a value 65 (not surfaced in the enum). - /// - /// - [Flags] - public enum Key : uint { - /// - /// Mask that indicates that this is a character value, values outside this range - /// indicate special characters like Alt-key combinations or special keys on the - /// keyboard like function keys, arrows keys and so on. - /// - CharMask = 0xfffff, - - /// - /// If the is set, then the value is that of the special mask, - /// otherwise, the value is the one of the lower bits (as extracted by ). - /// - SpecialMask = 0xfff00000, - - /// - /// The key code representing null or empty - /// - Null = '\0', - - /// - /// Backspace key. - /// - Backspace = 8, - - /// - /// The key code for the user pressing the tab key (forwards tab key). - /// - Tab = 9, - - /// - /// The key code for the user pressing the return key. - /// - Enter = '\n', - - /// - /// The key code for the user pressing the clear key. - /// - Clear = 12, - - /// - /// The key code for the user pressing the escape key - /// - Esc = 27, - - /// - /// The key code for the user pressing the space bar - /// - Space = 32, - - /// - /// Digit 0. - /// - D0 = 48, - /// - /// Digit 1. - /// - D1, - /// - /// Digit 2. - /// - D2, - /// - /// Digit 3. - /// - D3, - /// - /// Digit 4. - /// - D4, - /// - /// Digit 5. - /// - D5, - /// - /// Digit 6. - /// - D6, - /// - /// Digit 7. - /// - D7, - /// - /// Digit 8. - /// - D8, - /// - /// Digit 9. - /// - D9, - - /// - /// The key code for the user pressing Shift-A - /// - A = 65, - /// - /// The key code for the user pressing Shift-B - /// - B, - /// - /// The key code for the user pressing Shift-C - /// - C, - /// - /// The key code for the user pressing Shift-D - /// - D, - /// - /// The key code for the user pressing Shift-E - /// - E, - /// - /// The key code for the user pressing Shift-F - /// - F, - /// - /// The key code for the user pressing Shift-G - /// - G, - /// - /// The key code for the user pressing Shift-H - /// - H, - /// - /// The key code for the user pressing Shift-I - /// - I, - /// - /// The key code for the user pressing Shift-J - /// - J, - /// - /// The key code for the user pressing Shift-K - /// - K, - /// - /// The key code for the user pressing Shift-L - /// - L, - /// - /// The key code for the user pressing Shift-M - /// - M, - /// - /// The key code for the user pressing Shift-N - /// - N, - /// - /// The key code for the user pressing Shift-O - /// - O, - /// - /// The key code for the user pressing Shift-P - /// - P, - /// - /// The key code for the user pressing Shift-Q - /// - Q, - /// - /// The key code for the user pressing Shift-R - /// - R, - /// - /// The key code for the user pressing Shift-S - /// - S, - /// - /// The key code for the user pressing Shift-T - /// - T, - /// - /// The key code for the user pressing Shift-U - /// - U, - /// - /// The key code for the user pressing Shift-V - /// - V, - /// - /// The key code for the user pressing Shift-W - /// - W, - /// - /// The key code for the user pressing Shift-X - /// - X, - /// - /// The key code for the user pressing Shift-Y - /// - Y, - /// - /// The key code for the user pressing Shift-Z - /// - Z, - /// - /// The key code for the user pressing A - /// - a = 97, - /// - /// The key code for the user pressing B - /// - b, - /// - /// The key code for the user pressing C - /// - c, - /// - /// The key code for the user pressing D - /// - d, - /// - /// The key code for the user pressing E - /// - e, - /// - /// The key code for the user pressing F - /// - f, - /// - /// The key code for the user pressing G - /// - g, - /// - /// The key code for the user pressing H - /// - h, - /// - /// The key code for the user pressing I - /// - i, - /// - /// The key code for the user pressing J - /// - j, - /// - /// The key code for the user pressing K - /// - k, - /// - /// The key code for the user pressing L - /// - l, - /// - /// The key code for the user pressing M - /// - m, - /// - /// The key code for the user pressing N - /// - n, - /// - /// The key code for the user pressing O - /// - o, - /// - /// The key code for the user pressing P - /// - p, - /// - /// The key code for the user pressing Q - /// - q, - /// - /// The key code for the user pressing R - /// - r, - /// - /// The key code for the user pressing S - /// - s, - /// - /// The key code for the user pressing T - /// - t, - /// - /// The key code for the user pressing U - /// - u, - /// - /// The key code for the user pressing V - /// - v, - /// - /// The key code for the user pressing W - /// - w, - /// - /// The key code for the user pressing X - /// - x, - /// - /// The key code for the user pressing Y - /// - y, - /// - /// The key code for the user pressing Z - /// - z, - /// - /// The key code for the user pressing the delete key. - /// - Delete = 127, - - /// - /// When this value is set, the Key encodes the sequence Shift-KeyValue. - /// - ShiftMask = 0x10000000, - - /// - /// When this value is set, the Key encodes the sequence Alt-KeyValue. - /// And the actual value must be extracted by removing the AltMask. - /// - AltMask = 0x80000000, - - /// - /// When this value is set, the Key encodes the sequence Ctrl-KeyValue. - /// And the actual value must be extracted by removing the CtrlMask. - /// - CtrlMask = 0x40000000, - - /// - /// Cursor up key - /// - CursorUp = 0x100000, - /// - /// Cursor down key. - /// - CursorDown, - /// - /// Cursor left key. - /// - CursorLeft, - /// - /// Cursor right key. - /// - CursorRight, - /// - /// Page Up key. - /// - PageUp, - /// - /// Page Down key. - /// - PageDown, - /// - /// Home key. - /// - Home, - /// - /// End key. - /// - End, - - /// - /// Insert character key. - /// - InsertChar, - - /// - /// Delete character key. - /// - DeleteChar, - - /// - /// Shift-tab key (backwards tab key). - /// - BackTab, - - /// - /// Print screen character key. - /// - PrintScreen, - - /// - /// F1 key. - /// - F1, - /// - /// F2 key. - /// - F2, - /// - /// F3 key. - /// - F3, - /// - /// F4 key. - /// - F4, - /// - /// F5 key. - /// - F5, - /// - /// F6 key. - /// - F6, - /// - /// F7 key. - /// - F7, - /// - /// F8 key. - /// - F8, - /// - /// F9 key. - /// - F9, - /// - /// F10 key. - /// - F10, - /// - /// F11 key. - /// - F11, - /// - /// F12 key. - /// - F12, - /// - /// F13 key. - /// - F13, - /// - /// F14 key. - /// - F14, - /// - /// F15 key. - /// - F15, - /// - /// F16 key. - /// - F16, - /// - /// F17 key. - /// - F17, - /// - /// F18 key. - /// - F18, - /// - /// F19 key. - /// - F19, - /// - /// F20 key. - /// - F20, - /// - /// F21 key. - /// - F21, - /// - /// F22 key. - /// - F22, - /// - /// F23 key. - /// - F23, - /// - /// F24 key. - /// - F24, - - /// - /// A key with an unknown mapping was raised. - /// - Unknown - } - - /// - /// Describes a keyboard event. - /// - public class KeyEvent { - KeyModifiers keyModifiers; - - /// - /// Symbolic definition for the key. - /// - public Key Key; - - /// - /// The key value cast to an integer, you will typical use this for - /// extracting the Unicode rune value out of a key, when none of the - /// symbolic options are in use. - /// - public int KeyValue => (int)Key; - - /// - /// Gets a value indicating whether the Shift key was pressed. - /// - /// true if is shift; otherwise, false. - public bool IsShift => keyModifiers.Shift || Key == Key.BackTab; - - /// - /// Gets a value indicating whether the Alt key was pressed (real or synthesized) - /// - /// true if is alternate; otherwise, false. - public bool IsAlt => keyModifiers.Alt; - - /// - /// Determines whether the value is a control key (and NOT just the ctrl key) - /// - /// true if is ctrl; otherwise, false. - //public bool IsCtrl => ((uint)Key >= 1) && ((uint)Key <= 26); - public bool IsCtrl => keyModifiers.Ctrl; - - /// - /// Gets a value indicating whether the Caps lock key was pressed (real or synthesized) - /// - /// true if is alternate; otherwise, false. - public bool IsCapslock => keyModifiers.Capslock; - - /// - /// Gets a value indicating whether the Num lock key was pressed (real or synthesized) - /// - /// true if is alternate; otherwise, false. - public bool IsNumlock => keyModifiers.Numlock; - - /// - /// Gets a value indicating whether the Scroll lock key was pressed (real or synthesized) - /// - /// true if is alternate; otherwise, false. - public bool IsScrolllock => keyModifiers.Scrolllock; - - /// - /// Constructs a new - /// - public KeyEvent () - { - Key = Key.Unknown; - keyModifiers = new KeyModifiers (); - } - - /// - /// Constructs a new from the provided Key value - can be a rune cast into a Key value - /// - public KeyEvent (Key k, KeyModifiers km) - { - Key = k; - keyModifiers = km; - } - - /// - /// Pretty prints the KeyEvent - /// - /// - public override string ToString () - { - string msg = ""; - var key = this.Key; - if (keyModifiers.Shift) { - msg += "Shift-"; - } - if (keyModifiers.Alt) { - msg += "Alt-"; - } - if (keyModifiers.Ctrl) { - msg += "Ctrl-"; - } - if (keyModifiers.Capslock) { - msg += "Capslock-"; - } - if (keyModifiers.Numlock) { - msg += "Numlock-"; - } - if (keyModifiers.Scrolllock) { - msg += "Scrolllock-"; - } - - msg += $"{((Key)KeyValue != Key.Unknown && ((uint)this.KeyValue & (uint)Key.CharMask) > 27 ? $"{(char)this.KeyValue}" : $"{key}")}"; - - return msg; - } - } - - /// - /// Mouse flags reported in . - /// - /// - /// They just happen to map to the ncurses ones. - /// - [Flags] - public enum MouseFlags { - /// - /// The first mouse button was pressed. - /// - Button1Pressed = unchecked((int)0x2), - /// - /// The first mouse button was released. - /// - Button1Released = unchecked((int)0x1), - /// - /// The first mouse button was clicked (press+release). - /// - Button1Clicked = unchecked((int)0x4), - /// - /// The first mouse button was double-clicked. - /// - Button1DoubleClicked = unchecked((int)0x8), - /// - /// The first mouse button was triple-clicked. - /// - Button1TripleClicked = unchecked((int)0x10), - /// - /// The second mouse button was pressed. - /// - Button2Pressed = unchecked((int)0x80), - /// - /// The second mouse button was released. - /// - Button2Released = unchecked((int)0x40), - /// - /// The second mouse button was clicked (press+release). - /// - Button2Clicked = unchecked((int)0x100), - /// - /// The second mouse button was double-clicked. - /// - Button2DoubleClicked = unchecked((int)0x200), - /// - /// The second mouse button was triple-clicked. - /// - Button2TripleClicked = unchecked((int)0x400), - /// - /// The third mouse button was pressed. - /// - Button3Pressed = unchecked((int)0x2000), - /// - /// The third mouse button was released. - /// - Button3Released = unchecked((int)0x1000), - /// - /// The third mouse button was clicked (press+release). - /// - Button3Clicked = unchecked((int)0x4000), - /// - /// The third mouse button was double-clicked. - /// - Button3DoubleClicked = unchecked((int)0x8000), - /// - /// The third mouse button was triple-clicked. - /// - Button3TripleClicked = unchecked((int)0x10000), - /// - /// The fourth mouse button was pressed. - /// - Button4Pressed = unchecked((int)0x80000), - /// - /// The fourth mouse button was released. - /// - Button4Released = unchecked((int)0x40000), - /// - /// The fourth button was clicked (press+release). - /// - Button4Clicked = unchecked((int)0x100000), - /// - /// The fourth button was double-clicked. - /// - Button4DoubleClicked = unchecked((int)0x200000), - /// - /// The fourth button was triple-clicked. - /// - Button4TripleClicked = unchecked((int)0x400000), - /// - /// Flag: the shift key was pressed when the mouse button took place. - /// - ButtonShift = unchecked((int)0x2000000), - /// - /// Flag: the ctrl key was pressed when the mouse button took place. - /// - ButtonCtrl = unchecked((int)0x1000000), - /// - /// Flag: the alt key was pressed when the mouse button took place. - /// - ButtonAlt = unchecked((int)0x4000000), - /// - /// The mouse position is being reported in this event. - /// - ReportMousePosition = unchecked((int)0x8000000), - /// - /// Vertical button wheeled up. - /// - WheeledUp = unchecked((int)0x10000000), - /// - /// Vertical button wheeled down. - /// - WheeledDown = unchecked((int)0x20000000), - /// - /// Vertical button wheeled up while pressing ButtonShift. - /// - WheeledLeft = ButtonShift | WheeledUp, - /// - /// Vertical button wheeled down while pressing ButtonShift. - /// - WheeledRight = ButtonShift | WheeledDown, - /// - /// Mask that captures all the events. - /// - AllEvents = unchecked((int)0x7ffffff), - } - - // TODO: Merge MouseEvent and MouseEventEventArgs into a single class. - - /// - /// Low-level construct that conveys the details of mouse events, such - /// as coordinates and button state, from ConsoleDrivers up to and then to - /// Views. - /// - /// See and . - public class MouseEvent { - /// - /// The X (column) location for the mouse event relative to . - /// - public int X { get; set; } - - /// - /// The Y (column) location for the mouse event relative to . - /// - public int Y { get; set; } - - /// - /// Gets or sets the flags that indicate the kind of mouse event that is being posted. - /// - public MouseFlags Flags { get; set; } - - /// - /// Provides the X (column) mouse position offset from the grabbed view (see . - /// - /// - /// Calculated and processed in . - /// Whichever view that has called , will receive all the mouse event - /// with relative coordinates. The and provide - /// the screen-relative offset of these coordinates. - /// Using these properties, the view that has grabbed the mouse will know how much the mouse has moved. - /// - public int OfX { get; set; } - - /// - /// Provides the Y (row) mouse position offset from the grabbed view (see . - /// - /// - /// Calculated and processed in . - /// Whichever view that has called , will receive all the mouse event - /// with relative coordinates. The and provide - /// the screen-relative offset of these coordinates. - /// Using these properties, the view that has grabbed the mouse will know how much the mouse has moved. - /// - public int OfY { get; set; } - - /// - /// Gets or sets the view that should process the mouse event. - /// - public View View { get; set; } - - /// - /// Indicates if the mouse event has been handled by a view and other subscribers should ignore the event. - /// IMPORTANT: Set this value to when updating any View's layout from inside the subscriber method. - /// - public bool Handled { get; set; } - - /// - /// Returns a that represents the current . - /// - /// A that represents the current . - public override string ToString () - { - return $"({X},{Y}:{Flags}"; - } - } -} diff --git a/Terminal.Gui/Input/Key.cs b/Terminal.Gui/Input/Key.cs new file mode 100644 index 0000000000..67e5e09802 --- /dev/null +++ b/Terminal.Gui/Input/Key.cs @@ -0,0 +1,976 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text; +using System.Text.Json.Serialization; + +namespace Terminal.Gui; + +/// +/// Provides an abstraction for common keyboard operations and state. 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 enumeration for keyboard input whenever possible. +/// +/// +/// +/// +/// +/// The default value for is and can be tested using . +/// +/// +/// +/// +/// ConceptDefinition +/// +/// +/// Testing Shift State +/// +/// The Is properties (,, ) test for shift state; whether the key press was modified by a shift key. +/// +/// +/// +/// Adding Shift State +/// +/// The With properties (,, ) return a copy of the Key with the shift modifier applied. This +/// is useful for specifying a key that requires a shift modifier (e.g. var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel;. +/// +/// +/// +/// Removing Shift State +/// +/// The No properties (,, ) return a copy of the Key with the shift modifier removed. This +/// is useful for specifying a key that does not require a shift modifier (e.g. var ControlDelete = ControlAltDelete.NoCtrl;. +/// +/// +/// +/// Encoding of A..Z +/// +/// Lowercase alpha keys are encoded (in ) as values between 65 and 90 corresponding to +/// the un-shifted A to Z keys on a keyboard. Properties are provided for these (e.g. , , etc.). +/// Even though the encoded values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. +/// +/// +/// +/// Persistence as strings +/// +/// Keys are persisted as "[Modifiers]+[Key]. For example new Key(Key.Delete).WithAlt.WithDel is persisted as "Ctrl+Alt+Delete". See +/// and for more information. +/// +/// +/// +/// +/// +[JsonConverter (typeof (KeyJsonConverter))] +public class Key : EventArgs, IEquatable { + /// + /// Constructs a new + /// + public Key () : this (KeyCode.Null) { } + + /// + /// Constructs a new from the provided Key value + /// + /// The key + public Key (KeyCode k) => KeyCode = k; + + /// + /// Indicates if the current Key event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } = false; + + /// + /// The encoded key value. + /// + /// + /// IMPORTANT: Lowercase alpha keys are encoded (in ) as values between 65 and 90 corresponding to the un-shifted A to Z keys on a keyboard. Enum values + /// are provided for these (e.g. , , etc.). Even though the values are the same as the ASCII + /// values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. + /// + /// + /// This property is the backing data for the . It is a enum value. + /// + [JsonInclude] [JsonConverter (typeof (KeyCodeJsonConverter))] + public KeyCode KeyCode { get; set; } + + /// + /// Enables passing the key binding scope with the event. Default is . + /// + public KeyBindingScope Scope { get; set; } = KeyBindingScope.Focused; + + /// + /// The key value as a Rune. This is the actual value of the key pressed, and is independent of the modifiers. + /// + /// + /// If the key pressed is a letter (a-z or A-Z), this will be the upper or lower case letter depending on whether the shift key is pressed. + /// If the key is outside of the range, this will be . + /// + public Rune AsRune => ToRune (KeyCode); + + /// + /// Converts a to a . + /// + /// + /// If the key is a letter (a-z or A-Z), this will be the upper or lower case letter depending on whether the shift key is pressed. + /// If the key is outside of the range, this will be . + /// + /// + /// The key converted to a rune. if conversion is not possible. + public static Rune ToRune (KeyCode key) + { + if (key is KeyCode.Null or KeyCode.SpecialMask || key.HasFlag (KeyCode.CtrlMask) || key.HasFlag (KeyCode.AltMask)) { + return default; + } + + // Extract the base key (removing modifier flags) + var baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask; + + switch (baseKey) { + case >= KeyCode.A and <= KeyCode.Z when !key.HasFlag (KeyCode.ShiftMask): + return new Rune ((char)(baseKey + 32)); + case >= KeyCode.A and <= KeyCode.Z: + return new Rune ((char)baseKey); + case > KeyCode.Null and < KeyCode.A: + return new Rune ((char)baseKey); + } + + if (Enum.IsDefined (typeof (KeyCode), baseKey)) { + return default; + } + + return new Rune ((char)baseKey); + } + + /// + /// Gets a value indicating whether the Shift key was pressed. + /// + /// if is shift; otherwise, . + public bool IsShift => (KeyCode & KeyCode.ShiftMask) != 0; + + /// + /// Gets a value indicating whether the Alt key was pressed (real or synthesized) + /// + /// if is alternate; otherwise, . + public bool IsAlt => (KeyCode & KeyCode.AltMask) != 0; + + /// + /// Gets a value indicating whether the Ctrl key was pressed. + /// + /// if is ctrl; otherwise, . + public bool IsCtrl => (KeyCode & KeyCode.CtrlMask) != 0; + + /// + /// Gets a value indicating whether the KeyCode is composed of a lower case letter from 'a' to 'z', independent of the shift key. + /// + /// + /// IMPORTANT: Lowercase alpha keys are encoded in as values between 65 and 90 corresponding to + /// the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g. , , etc.). + /// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. + /// + public bool IsKeyCodeAtoZ => GetIsKeyCodeAtoZ (KeyCode); + + /// + /// Tests if a KeyCode is composed of a lower case letter from 'a' to 'z', independent of the shift key. + /// + /// + /// IMPORTANT: Lowercase alpha keys are encoded in as values between 65 and 90 corresponding to + /// the un-shifted A to Z keys on a keyboard. Helper properties are provided these (e.g. , , etc.). + /// Even though the values are the same as the ASCII values for uppercase characters, these enum values represent *lowercase*, un-shifted characters. + /// + public static bool GetIsKeyCodeAtoZ (KeyCode keyCode) + { + if ((keyCode & KeyCode.AltMask) != 0 || (keyCode & KeyCode.CtrlMask) != 0) { + return false; + } + + if ((keyCode & ~KeyCode.Space & ~KeyCode.ShiftMask) is >= KeyCode.A and <= KeyCode.Z) { + return true; + } + + return (keyCode & KeyCode.CharMask) is >= KeyCode.A and <= KeyCode.Z; + } + + /// + /// Indicates whether the is valid or not. + /// + public bool IsValid => KeyCode is not (KeyCode.Null or KeyCode.Unknown); + + /// + /// Helper for specifying a shifted . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// + /// + public Key WithShift => new (KeyCode | KeyCode.ShiftMask); + + /// + /// Helper for removing a shift modifier from a . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// var AltDelete = ControlAltDelete.NoCtrl; + /// + /// + public Key NoShift => new (KeyCode & ~KeyCode.ShiftMask); + + /// + /// Helper for specifying a shifted . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// + /// + public Key WithCtrl => new (KeyCode | KeyCode.CtrlMask); + + /// + /// Helper for removing a shift modifier from a . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// var AltDelete = ControlAltDelete.NoCtrl; + /// + /// + public Key NoCtrl => new (KeyCode & ~KeyCode.CtrlMask); + + /// + /// Helper for specifying a shifted . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// + /// + public Key WithAlt => new (KeyCode | KeyCode.AltMask); + + /// + /// Helper for removing a shift modifier from a . + /// + /// var ControlAltDelete = new Key(Key.Delete).WithAlt.WithDel; + /// var AltDelete = ControlAltDelete.NoCtrl; + /// + /// + public Key NoAlt => new (KeyCode & ~KeyCode.AltMask); + + #region Operators + /// + /// Explicitly cast a to a . The conversion is lossy. + /// + /// + /// Uses . + /// + /// + public static explicit operator Rune (Key kea) => kea.AsRune; + + /// + /// Explicitly cast to a . The conversion is lossy. + /// + /// + public static explicit operator char (Key kea) => (char)kea.AsRune.Value; + + /// + /// Explicitly cast to a . The conversion is lossy. + /// + /// + public static explicit operator KeyCode (Key key) => key.KeyCode; + + /// + /// Cast to a . + /// + /// + public static implicit operator Key (KeyCode keyCode) => new (keyCode); + + + /// + /// Cast to a . + /// + /// + public static implicit operator Key (char ch) => new ((KeyCode)ch); + + /// + public override bool Equals (object obj) => obj is Key k && k.KeyCode == KeyCode; + + bool IEquatable.Equals (Key other) => Equals ((object)other); + + /// + public override int GetHashCode () => (int)KeyCode; + + /// + /// + /// + /// + /// + public static bool operator == (Key a, Key b) => a?.KeyCode == b?.KeyCode; + + /// + /// + /// + /// + /// + public static bool operator != (Key a, Key b) => a?.KeyCode != b?.KeyCode; + + /// + /// Compares two s for less-than. + /// + /// + /// + /// + public static bool operator < (Key a, Key b) => a?.KeyCode < b?.KeyCode; + + /// + /// Compares two s for greater-than. + /// + /// + /// + /// + public static bool operator > (Key a, Key b) => a?.KeyCode > b?.KeyCode; + + /// + /// Compares two s for greater-than-or-equal-to. + /// + /// + /// + /// + public static bool operator <= (Key a, Key b) => a?.KeyCode <= b?.KeyCode; + + /// + /// Compares two s for greater-than-or-equal-to. + /// + /// + /// + /// + public static bool operator >= (Key a, Key b) => a?.KeyCode >= b?.KeyCode; + #endregion Operators + + #region String conversion + /// + /// Pretty prints the KeyEvent + /// + /// + public override string ToString () => ToString (KeyCode, (Rune)'+'); + + static string GetKeyString (KeyCode key) + { + if (key is KeyCode.Null or KeyCode.SpecialMask) { + return string.Empty; + } + // Extract the base key (removing modifier flags) + var baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask; + + if (!key.HasFlag (KeyCode.ShiftMask) && baseKey is >= KeyCode.A and <= KeyCode.Z) { + return ((char)(key + 32)).ToString (); + } + + if (key is >= KeyCode.Space and < KeyCode.A) { + return ((char)key).ToString (); + } + + string keyName = Enum.GetName (typeof (KeyCode), key); + return !string.IsNullOrEmpty (keyName) ? keyName : ((char)key).ToString (); + } + + + /// + /// Formats a as a string using the default separator of '+' + /// + /// The key to format. + /// The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key name will be returned. + public static string ToString (KeyCode key) => ToString (key, (Rune)'+'); + + /// + /// Formats a as a string. + /// + /// The key to format. + /// The character to use as a separator between modifier keys and and the key itself. + /// The formatted string. If the key is a printable character, it will be returned as a string. Otherwise, the key name will be returned. + public static string ToString (KeyCode key, Rune separator) + { + if (key is KeyCode.Null) { + return string.Empty; + } + + var sb = new StringBuilder (); + + // Extract the base key (removing modifier flags) + var baseKey = key & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask; + + // Extract and handle modifiers + bool hasModifiers = false; + if ((key & KeyCode.CtrlMask) != 0) { + sb.Append ($"Ctrl{separator}"); + hasModifiers = true; + } + if ((key & KeyCode.AltMask) != 0) { + sb.Append ($"Alt{separator}"); + hasModifiers = true; + } + if ((key & KeyCode.ShiftMask) != 0 && !GetIsKeyCodeAtoZ (key)) { + sb.Append ($"Shift{separator}"); + hasModifiers = true; + } + + // Handle special cases and modifiers on their own + if (key != KeyCode.SpecialMask && (baseKey != KeyCode.Null || hasModifiers)) { + if ((key & KeyCode.SpecialMask) != 0 && (baseKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z) { + sb.Append (baseKey & ~KeyCode.Space); + } else { + // Append the actual key name + sb.Append (GetKeyString (baseKey)); + } + } + + string result = sb.ToString (); + result = TrimEndRune (result, separator); + return result; + } + + static string TrimEndRune (string input, Rune runeToTrim) + { + // Convert the Rune to a string (which may be one or two chars) + string runeString = runeToTrim.ToString (); + + if (input.EndsWith (runeString)) { + // Remove the rune from the end of the string + return input.Substring (0, input.Length - runeString.Length); + } + + return input; + } + + static readonly Dictionary _modifierDict = new (comparer: StringComparer.InvariantCultureIgnoreCase) { + { "Shift", KeyCode.ShiftMask }, + { "Ctrl", KeyCode.CtrlMask }, + { "Alt", KeyCode.AltMask } + }; + + /// + /// Converts the provided string to a new instance. + /// + /// The text to analyze. Formats supported are + /// "Ctrl+X", "Alt+X", "Shift+X", "Ctrl+Alt+X", "Ctrl+Shift+X", "Alt+Shift+X", "Ctrl+Alt+Shift+X", and "X". + /// + /// The parsed value. + /// A boolean value indicating whether parsing was successful. + /// + /// + public static bool TryParse (string text, [NotNullWhen (true)] out Key key) + { + if (string.IsNullOrEmpty (text)) { + key = new Key (KeyCode.Null); + return true; + } + + key = null; + + // Split the string into parts + string [] parts = text.Split ('+', '-'); + + if (parts.Length is 0 or > 4 || parts.Any (string.IsNullOrEmpty)) { + return false; + } + + // if it's just a shift key + if (parts.Length == 1) { + switch (parts [0]) { + case "Ctrl": + key = new Key (KeyCode.CtrlKey); + return true; + case "Alt": + key = new Key (KeyCode.AltKey); + return true; + case "Shift": + key = new Key (KeyCode.ShiftKey); + return true; + } + } + + var modifiers = KeyCode.Null; + for (int index = 0; index < parts.Length; index++) { + if (_modifierDict.TryGetValue (parts [index].ToLowerInvariant (), out var modifier)) { + modifiers |= modifier; + parts [index] = string.Empty; // eat it + } + } + + // we now have the modifiers + + string partNotFound = parts.FirstOrDefault (p => !string.IsNullOrEmpty (p), string.Empty); + var parsedKeyCode = KeyCode.Null; + int parsedInt = 0; + if (partNotFound.Length == 1) { + var keyCode = (KeyCode)partNotFound [0]; + // if it's a single digit int, treat it as such + if (int.TryParse (partNotFound, + System.Globalization.NumberStyles.Integer, + System.Globalization.CultureInfo.InvariantCulture, + out parsedInt)) { + keyCode = (KeyCode)((int)KeyCode.D0 + parsedInt); + } else if (Enum.TryParse (partNotFound, false, out parsedKeyCode)) { + if (parsedKeyCode != KeyCode.Null) { + if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) { + key = new Key (parsedKeyCode | KeyCode.ShiftMask); + return true; + } + key = new Key ((KeyCode)parsedKeyCode | modifiers); + return true; + } + } + key = new Key (keyCode | modifiers); + return true; + } + + if (Enum.TryParse (partNotFound, true, out parsedKeyCode)) { + if (parsedKeyCode != KeyCode.Null) { + if (parsedKeyCode is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) { + key = new Key (parsedKeyCode | KeyCode.ShiftMask); + return true; + } + key = new Key (parsedKeyCode | modifiers); + return true; + } + } + + // if it's a number int, treat it as a unicode value + if (int.TryParse (partNotFound, + System.Globalization.NumberStyles.Number, + System.Globalization.CultureInfo.InvariantCulture, out parsedInt)) { + if (!Rune.IsValid (parsedInt)) { + return false; + } + if ((KeyCode)parsedInt is >= KeyCode.A and <= KeyCode.Z && modifiers == 0) { + key = new Key ((KeyCode)parsedInt | KeyCode.ShiftMask); + return true; + } + key = new Key ((KeyCode)parsedInt); + return true; + } + + if (!Enum.TryParse (partNotFound, true, out parsedKeyCode)) { + return false; + } + + if (GetIsKeyCodeAtoZ (parsedKeyCode)) { + key = new Key (parsedKeyCode | modifiers & ~KeyCode.Space); + return true; + } + + return false; + } + #endregion + + + #region Standard Key Definitions + /// + /// An uninitialized The object. + /// + public new static readonly Key Empty = new (); + + /// + /// The object for the Backspace key. + /// + public static readonly Key Backspace = new (KeyCode.Backspace); + + /// + /// The object for the tab key (forwards tab key). + /// + public static readonly Key Tab = new (KeyCode.Tab); + + /// + /// The object for the return key. + /// + public static readonly Key Enter = new (KeyCode.Enter); + + /// + /// The object for the clear key. + /// + public static readonly Key Clear = new (KeyCode.Clear); + + /// + /// The object for the Shift key. + /// + public static readonly Key Shift = new (KeyCode.ShiftKey); + + /// + /// The object for the Ctrl key. + /// + public static readonly Key Ctrl = new (KeyCode.CtrlKey); + + /// + /// The object for the Alt key. + /// + public static readonly Key Alt = new (KeyCode.AltKey); + + /// + /// The object for the CapsLock key. + /// + public static readonly Key CapsLock = new (KeyCode.CapsLock); + + /// + /// The object for the Escape key. + /// + public static readonly Key Esc = new (KeyCode.Esc); + + /// + /// The object for the Space bar key. + /// + public static readonly Key Space = new (KeyCode.Space); + + /// + /// The object for 0 key. + /// + public static readonly Key D0 = new (KeyCode.D0); + + /// + /// The object for 1 key. + /// + public static readonly Key D1 = new (KeyCode.D1); + + /// + /// The object for 2 key. + /// + public static readonly Key D2 = new (KeyCode.D2); + + /// + /// The object for 3 key. + /// + public static readonly Key D3 = new (KeyCode.D3); + + /// + /// The object for 4 key. + /// + public static readonly Key D4 = new (KeyCode.D4); + + /// + /// The object for 5 key. + /// + public static readonly Key D5 = new (KeyCode.D5); + + /// + /// The object for 6 key. + /// + public static readonly Key D6 = new (KeyCode.D6); + + /// + /// The object for 7 key. + /// + public static readonly Key D7 = new (KeyCode.D7); + + /// + /// The object for 8 key. + /// + public static readonly Key D8 = new (KeyCode.D8); + + /// + /// The object for 9 key. + /// + public static readonly Key D9 = new (KeyCode.D9); + + /// + /// The object for the A key (un-shifted). Use Key.A.WithShift for uppercase 'A'. + /// + public static readonly Key A = new (KeyCode.A); + + /// + /// The object for the B key (un-shifted). Use Key.B.WithShift for uppercase 'B'. + /// + public static readonly Key B = new (KeyCode.B); + + /// + /// The object for the C key (un-shifted). Use Key.C.WithShift for uppercase 'C'. + /// + public static readonly Key C = new (KeyCode.C); + + /// + /// The object for the D key (un-shifted). Use Key.D.WithShift for uppercase 'D'. + /// + public static readonly Key D = new (KeyCode.D); + + /// + /// The object for the E key (un-shifted). Use Key.E.WithShift for uppercase 'E'. + /// + public static readonly Key E = new (KeyCode.E); + + /// + /// The object for the F key (un-shifted). Use Key.F.WithShift for uppercase 'F'. + /// + public static readonly Key F = new (KeyCode.F); + + /// + /// The object for the G key (un-shifted). Use Key.G.WithShift for uppercase 'G'. + /// + public static readonly Key G = new (KeyCode.G); + + /// + /// The object for the H key (un-shifted). Use Key.H.WithShift for uppercase 'H'. + /// + public static readonly Key H = new (KeyCode.H); + + /// + /// The object for the I key (un-shifted). Use Key.I.WithShift for uppercase 'I'. + /// + public static readonly Key I = new (KeyCode.I); + + /// + /// The object for the J key (un-shifted). Use Key.J.WithShift for uppercase 'J'. + /// + public static readonly Key J = new (KeyCode.J); + + /// + /// The object for the K key (un-shifted). Use Key.K.WithShift for uppercase 'K'. + /// + public static readonly Key K = new (KeyCode.K); + + /// + /// The object for the L key (un-shifted). Use Key.L.WithShift for uppercase 'L'. + /// + public static readonly Key L = new (KeyCode.L); + + /// + /// The object for the M key (un-shifted). Use Key.M.WithShift for uppercase 'M'. + /// + public static readonly Key M = new (KeyCode.M); + + /// + /// The object for the N key (un-shifted). Use Key.N.WithShift for uppercase 'N'. + /// + public static readonly Key N = new (KeyCode.N); + + /// + /// The object for the O key (un-shifted). Use Key.O.WithShift for uppercase 'O'. + /// + public static readonly Key O = new (KeyCode.O); + + /// + /// The object for the P key (un-shifted). Use Key.P.WithShift for uppercase 'P'. + /// + public static readonly Key P = new (KeyCode.P); + + /// + /// The object for the Q key (un-shifted). Use Key.Q.WithShift for uppercase 'Q'. + /// + public static readonly Key Q = new (KeyCode.Q); + + /// + /// The object for the R key (un-shifted). Use Key.R.WithShift for uppercase 'R'. + /// + public static readonly Key R = new (KeyCode.R); + + /// + /// The object for the S key (un-shifted). Use Key.S.WithShift for uppercase 'S'. + /// + public static readonly Key S = new (KeyCode.S); + + /// + /// The object for the T key (un-shifted). Use Key.T.WithShift for uppercase 'T'. + /// + public static readonly Key T = new (KeyCode.T); + + /// + /// The object for the U key (un-shifted). Use Key.U.WithShift for uppercase 'U'. + /// + public static readonly Key U = new (KeyCode.U); + + /// + /// The object for the V key (un-shifted). Use Key.V.WithShift for uppercase 'V'. + /// + public static readonly Key V = new (KeyCode.V); + + /// + /// The object for the W key (un-shifted). Use Key.W.WithShift for uppercase 'W'. + /// + public static readonly Key W = new (KeyCode.W); + + /// + /// The object for the X key (un-shifted). Use Key.X.WithShift for uppercase 'X'. + /// + public static readonly Key X = new (KeyCode.X); + + /// + /// The object for the Y key (un-shifted). Use Key.Y.WithShift for uppercase 'Y'. + /// + public static readonly Key Y = new (KeyCode.Y); + + /// + /// The object for the Z key (un-shifted). Use Key.Z.WithShift for uppercase 'Z'. + /// + public static readonly Key Z = new (KeyCode.Z); + + /// + /// The object for the Delete key. + /// + public static readonly Key Delete = new (KeyCode.Delete); + + /// + /// The object for the Cursor up key. + /// + public static readonly Key CursorUp = new (KeyCode.CursorUp); + + /// + /// The object for Cursor down key. + /// + public static readonly Key CursorDown = new (KeyCode.CursorDown); + + /// + /// The object for Cursor left key. + /// + public static readonly Key CursorLeft = new (KeyCode.CursorLeft); + + /// + /// The object for Cursor right key. + /// + public static readonly Key CursorRight = new (KeyCode.CursorRight); + + /// + /// The object for Page Up key. + /// + public static readonly Key PageUp = new (KeyCode.PageUp); + + /// + /// The object for Page Down key. + /// + public static readonly Key PageDown = new (KeyCode.PageDown); + + /// + /// The object for Home key. + /// + public static readonly Key Home = new (KeyCode.Home); + + /// + /// The object for End key. + /// + public static readonly Key End = new (KeyCode.End); + + /// + /// The object for Insert Character key. + /// + public static readonly Key InsertChar = new (KeyCode.InsertChar); + + /// + /// The object for Delete Character key. + /// + public static readonly Key DeleteChar = new (KeyCode.DeleteChar); + + /// + /// The object for Print Screen key. + /// + public static readonly Key PrintScreen = new (KeyCode.PrintScreen); + + /// + /// The object for F1 key. + /// + public static readonly Key F1 = new (KeyCode.F1); + + /// + /// The object for F2 key. + /// + public static readonly Key F2 = new (KeyCode.F2); + + /// + /// The object for F3 key. + /// + public static readonly Key F3 = new (KeyCode.F3); + + /// + /// The object for F4 key. + /// + public static readonly Key F4 = new (KeyCode.F4); + + /// + /// The object for F5 key. + /// + public static readonly Key F5 = new (KeyCode.F5); + + /// + /// The object for F6 key. + /// + public static readonly Key F6 = new (KeyCode.F6); + + /// + /// The object for F7 key. + /// + public static readonly Key F7 = new (KeyCode.F7); + + /// + /// The object for F8 key. + /// + public static readonly Key F8 = new (KeyCode.F8); + + /// + /// The object for F9 key. + /// + public static readonly Key F9 = new (KeyCode.F9); + + /// + /// The object for F10 key. + /// + public static readonly Key F10 = new (KeyCode.F10); + + /// + /// The object for F11 key. + /// + public static readonly Key F11 = new (KeyCode.F11); + + /// + /// The object for F12 key. + /// + public static readonly Key F12 = new (KeyCode.F12); + + /// + /// The object for F13 key. + /// + public static readonly Key F13 = new (KeyCode.F13); + + /// + /// The object for F14 key. + /// + public static readonly Key F14 = new (KeyCode.F14); + + /// + /// The object for F15 key. + /// + public static readonly Key F15 = new (KeyCode.F15); + + /// + /// The object for F16 key. + /// + public static readonly Key F16 = new (KeyCode.F16); + + /// + /// The object for F17 key. + /// + public static readonly Key F17 = new (KeyCode.F17); + + /// + /// The object for F18 key. + /// + public static readonly Key F18 = new (KeyCode.F18); + + /// + /// The object for F19 key. + /// + public static readonly Key F19 = new (KeyCode.F19); + + /// + /// The object for F20 key. + /// + public static readonly Key F20 = new (KeyCode.F20); + + /// + /// The object for F21 key. + /// + public static readonly Key F21 = new (KeyCode.F21); + + /// + /// The object for F22 key. + /// + public static readonly Key F22 = new (KeyCode.F22); + + /// + /// The object for F23 key. + /// + public static readonly Key F23 = new (KeyCode.F23); + + /// + /// The object for F24 key. + /// + public static readonly Key F24 = new (KeyCode.F24); + #endregion +} \ No newline at end of file diff --git a/Terminal.Gui/Input/KeyBinding.cs b/Terminal.Gui/Input/KeyBinding.cs new file mode 100644 index 0000000000..e9a4354cc1 --- /dev/null +++ b/Terminal.Gui/Input/KeyBinding.cs @@ -0,0 +1,286 @@ +// These classes use a key binding system based on the design implemented in Scintilla.Net which is an +// MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui; + +/// +/// Defines the scope of a that has been bound to a key with . +/// +/// +/// +/// Key bindings are scoped to the most-focused view () by default. +/// +/// +public enum KeyBindingScope { + /// + /// The key binding is scoped to just the view that has focus. + /// + Focused = 0, + + /// + /// The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as long as the + /// SuperView does have focus. This is typically used for s. + /// + /// + /// Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even when not focused. + /// + /// + /// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or any of its subviews. + /// + /// + /// + HotKey, + + /// + /// The key binding will be triggered regardless of which view has focus. This is typically used for global commands. + /// + /// + /// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or any of its subviews, + /// and if the key down event was not bound to a . + /// + Application +} + +/// +/// Provides a collection of objects that are scoped to . +/// +public class KeyBinding { + /// + /// Initializes a new instance. + /// + /// + /// + public KeyBinding (Command [] commands, KeyBindingScope scope) + { + Commands = commands; + Scope = scope; + } + + /// + /// The actions which can be performed by the application or bound to keys in a control. + /// + public Command [] Commands { get; set; } + + /// + /// The scope of the bound to a key. + /// + public KeyBindingScope Scope { get; set; } +} + +/// +/// A class that provides a collection of objects bound to a . +/// +public class KeyBindings { + /// + /// The collection of objects. + /// + public Dictionary Bindings { get; } = new (); + + /// + /// Adds a to the collection. + /// + /// + /// + public void Add (Key key, KeyBinding binding) => Bindings.Add (key, binding); + + /// + /// Removes a from the collection. + /// + /// + public void Remove (Key key) => Bindings.Remove (key); + + /// + /// Removes all objects from the collection. + /// + public void Clear () => Bindings.Clear (); + + /// + /// + /// Adds a new key combination that will trigger the commands in . + /// + /// + /// If the key is already bound to a different array of s it will be + /// rebound . + /// + /// + /// Commands are only ever applied to the current (i.e. this feature + /// cannot be used to switch focus to another view and perform multiple commands there). + /// + /// + /// The key to check. + /// + /// The scope for the command. + /// The command to invoked on the when is pressed. + /// When multiple commands are provided,they will be applied in sequence. The bound strike + /// will be consumed if any took effect. + public void Add (Key key, KeyBindingScope scope, params Command [] commands) + { + if (commands.Length == 0) { + throw new ArgumentException (@"At least one command must be specified", nameof (commands)); + } + + if (key == null || !key.IsValid) { + //throw new ArgumentException ("Invalid Key", nameof (commands)); + return; + } + + if (TryGet (key, out var _)) { + Bindings [key] = new KeyBinding (commands, scope); + } else { + Bindings.Add (key, new KeyBinding (commands, scope)); + } + } + + /// + /// + /// Adds a new key combination that will trigger the commands in + /// (if supported by the View - see ). + /// + /// + /// This is a helper function for + /// for scoped commands. + /// + /// + /// If the key is already bound to a different array of s it will be + /// rebound . + /// + /// + /// Commands are only ever applied to the current (i.e. this feature + /// cannot be used to switch focus to another view and perform multiple commands there). + /// + /// + /// The key to check. + /// + /// The command to invoked on the when is pressed. + /// When multiple commands are provided,they will be applied in sequence. The bound strike + /// will be consumed if any took effect. + public void Add (Key key, params Command [] commands) => Add (key, KeyBindingScope.Focused, commands); + + /// + /// Replaces a key combination already bound to a set of s. + /// + /// + /// + /// The key to be replaced. + /// The new key to be used. + public void Replace (Key fromKey, Key toKey) + { + if (!TryGet (fromKey, out var _)) { + return; + } + var value = Bindings [fromKey]; + Bindings.Remove (fromKey); + Bindings [toKey] = value; + } + + /// + /// Gets the commands bound with the specified Key. + /// + /// + /// + /// + /// The key to check. + /// + /// + /// When this method returns, contains the commands bound with the specified Key, if the Key is found; + /// otherwise, null. This parameter is passed uninitialized. + /// + /// + /// if the Key is bound; otherwise . + /// + public bool TryGet (Key key, out KeyBinding binding) + { + if (key.IsValid) { + return Bindings.TryGetValue (key, out binding); + } + binding = new KeyBinding (Array.Empty (), KeyBindingScope.Focused); + return false; + } + + /// + /// Gets the for the specified . + /// + /// + /// + public KeyBinding Get (Key key) => TryGet (key, out var binding) ? binding : null; + + /// + /// Gets the commands bound with the specified Key that are scoped to a particular scope. + /// + /// + /// + /// + /// The key to check. + /// + /// the scope to filter on + /// + /// When this method returns, contains the commands bound with the specified Key, if the Key is found; + /// otherwise, null. This parameter is passed uninitialized. + /// + /// + /// if the Key is bound; otherwise . + /// + public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding) + { + if (key.IsValid && Bindings.TryGetValue (key, out binding)) { + if (binding.Scope == scope) { + return true; + } + } + binding = new KeyBinding (Array.Empty (), KeyBindingScope.Focused); + return false; + } + + /// + /// Gets the for the specified . + /// + /// + /// + /// + public KeyBinding Get (Key key, KeyBindingScope scope) => TryGet (key, scope, out var binding) ? binding : null; + + /// + /// Gets the array of s bound to if it exists. + /// + /// + /// The key to check. + /// + /// The array of s if is bound. An empty array if not. + public Command [] GetCommands (Key key) + { + if (TryGet (key, out var bindings)) { + return bindings.Commands; + } + return Array.Empty (); + } + + /// + /// Removes all key bindings that trigger the given command set. Views can have multiple different + /// keys bound to the same command sets and this method will clear all of them. + /// + /// + public void Clear (params Command [] command) + { + foreach (var kvp in Bindings.Where (kvp => kvp.Value.Commands.SequenceEqual (command)).ToArray ()) { + Bindings.Remove (kvp.Key); + } + } + + /// + /// Gets the Key used by a set of commands. + /// + /// + /// + /// The set of commands to search. + /// The used by a + /// If no matching set of commands was found. + public Key GetKeyFromCommands (params Command [] commands) + { + return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; + } +} + diff --git a/Terminal.Gui/Input/KeyChangedEventArgs.cs b/Terminal.Gui/Input/KeyChangedEventArgs.cs index eb1b37e9a6..459d07a469 100644 --- a/Terminal.Gui/Input/KeyChangedEventArgs.cs +++ b/Terminal.Gui/Input/KeyChangedEventArgs.cs @@ -1,34 +1,33 @@ using System; -namespace Terminal.Gui { +namespace Terminal.Gui; + +/// +/// Event args for when a is changed from +/// one value to a new value (e.g. in ) +/// +public class KeyChangedEventArgs : EventArgs { /// - /// Event args for when a is changed from - /// one value to a new value (e.g. in ) + /// Gets the old that was set before the event. + /// Use to check for empty. /// - public class KeyChangedEventArgs : EventArgs { - - /// - /// Gets the old that was set before the event. - /// Use to check for empty. - /// - public Key OldKey { get; } + public Key OldKey { get; } - /// - /// Gets the new that is being used. - /// Use to check for empty. - /// - public Key NewKey { get; } + /// + /// Gets the new that is being used. + /// Use to check for empty. + /// + public Key NewKey { get; } - /// - /// Creates a new instance of the class - /// - /// - /// - public KeyChangedEventArgs (Key oldKey, Key newKey) - { - this.OldKey = oldKey; - this.NewKey = newKey; - } + /// + /// Creates a new instance of the class + /// + /// + /// + public KeyChangedEventArgs (Key oldKey, Key newKey) + { + this.OldKey = oldKey; + this.NewKey = newKey; } } diff --git a/Terminal.Gui/Input/KeyEventEventArgs.cs b/Terminal.Gui/Input/KeyEventEventArgs.cs deleted file mode 100644 index c7ab9e39a2..0000000000 --- a/Terminal.Gui/Input/KeyEventEventArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; - -namespace Terminal.Gui { - - /// - /// Defines the event arguments for - /// - public class KeyEventEventArgs : EventArgs { - /// - /// Constructs. - /// - /// - public KeyEventEventArgs (KeyEvent ke) => KeyEvent = ke; - /// - /// The for the event. - /// - public KeyEvent KeyEvent { get; set; } - /// - /// Indicates if the current Key event has already been processed and the driver should stop notifying any other event subscriber. - /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. - /// - public bool Handled { get; set; } = false; - } -} diff --git a/Terminal.Gui/Input/Mouse.cs b/Terminal.Gui/Input/Mouse.cs new file mode 100644 index 0000000000..5ee6a75f8f --- /dev/null +++ b/Terminal.Gui/Input/Mouse.cs @@ -0,0 +1,185 @@ +using System; + +namespace Terminal.Gui; + +/// +/// Mouse flags reported in . +/// +/// +/// They just happen to map to the ncurses ones. +/// +[Flags] +public enum MouseFlags { + /// + /// The first mouse button was pressed. + /// + Button1Pressed = unchecked((int)0x2), + /// + /// The first mouse button was released. + /// + Button1Released = unchecked((int)0x1), + /// + /// The first mouse button was clicked (press+release). + /// + Button1Clicked = unchecked((int)0x4), + /// + /// The first mouse button was double-clicked. + /// + Button1DoubleClicked = unchecked((int)0x8), + /// + /// The first mouse button was triple-clicked. + /// + Button1TripleClicked = unchecked((int)0x10), + /// + /// The second mouse button was pressed. + /// + Button2Pressed = unchecked((int)0x80), + /// + /// The second mouse button was released. + /// + Button2Released = unchecked((int)0x40), + /// + /// The second mouse button was clicked (press+release). + /// + Button2Clicked = unchecked((int)0x100), + /// + /// The second mouse button was double-clicked. + /// + Button2DoubleClicked = unchecked((int)0x200), + /// + /// The second mouse button was triple-clicked. + /// + Button2TripleClicked = unchecked((int)0x400), + /// + /// The third mouse button was pressed. + /// + Button3Pressed = unchecked((int)0x2000), + /// + /// The third mouse button was released. + /// + Button3Released = unchecked((int)0x1000), + /// + /// The third mouse button was clicked (press+release). + /// + Button3Clicked = unchecked((int)0x4000), + /// + /// The third mouse button was double-clicked. + /// + Button3DoubleClicked = unchecked((int)0x8000), + /// + /// The third mouse button was triple-clicked. + /// + Button3TripleClicked = unchecked((int)0x10000), + /// + /// The fourth mouse button was pressed. + /// + Button4Pressed = unchecked((int)0x80000), + /// + /// The fourth mouse button was released. + /// + Button4Released = unchecked((int)0x40000), + /// + /// The fourth button was clicked (press+release). + /// + Button4Clicked = unchecked((int)0x100000), + /// + /// The fourth button was double-clicked. + /// + Button4DoubleClicked = unchecked((int)0x200000), + /// + /// The fourth button was triple-clicked. + /// + Button4TripleClicked = unchecked((int)0x400000), + /// + /// Flag: the shift key was pressed when the mouse button took place. + /// + ButtonShift = unchecked((int)0x2000000), + /// + /// Flag: the ctrl key was pressed when the mouse button took place. + /// + ButtonCtrl = unchecked((int)0x1000000), + /// + /// Flag: the alt key was pressed when the mouse button took place. + /// + ButtonAlt = unchecked((int)0x4000000), + /// + /// The mouse position is being reported in this event. + /// + ReportMousePosition = unchecked((int)0x8000000), + /// + /// Vertical button wheeled up. + /// + WheeledUp = unchecked((int)0x10000000), + /// + /// Vertical button wheeled down. + /// + WheeledDown = unchecked((int)0x20000000), + /// + /// Vertical button wheeled up while pressing ButtonShift. + /// + WheeledLeft = ButtonShift | WheeledUp, + /// + /// Vertical button wheeled down while pressing ButtonShift. + /// + WheeledRight = ButtonShift | WheeledDown, + /// + /// Mask that captures all the events. + /// + AllEvents = unchecked((int)0x7ffffff), +} + +// TODO: Merge MouseEvent and MouseEventEventArgs into a single class. + +/// +/// Low-level construct that conveys the details of mouse events, such +/// as coordinates and button state, from ConsoleDrivers up to and +/// Views. +/// +/// The class includes the +/// Action which takes a MouseEvent argument. +public class MouseEvent { + /// + /// The X (column) location for the mouse event. + /// + public int X { get; set; } + + /// + /// The Y (column) location for the mouse event. + /// + public int Y { get; set; } + + /// + /// Flags indicating the kind of mouse event that is being posted. + /// + public MouseFlags Flags { get; set; } + + /// + /// The offset X (column) location for the mouse event. + /// + public int OfX { get; set; } + + /// + /// The offset Y (column) location for the mouse event. + /// + public int OfY { get; set; } + + /// + /// The current view at the location for the mouse event. + /// + public View View { get; set; } + + /// + /// Indicates if the current mouse event has already been processed and the driver should stop notifying any other event subscriber. + /// Its important to set this value to true specially when updating any View's layout from inside the subscriber method. + /// + public bool Handled { get; set; } + + /// + /// Returns a that represents the current . + /// + /// A that represents the current . + public override string ToString () + { + return $"({X},{Y}:{Flags}"; + } +} \ No newline at end of file diff --git a/Terminal.Gui/Input/Responder.cs b/Terminal.Gui/Input/Responder.cs index e390f4a0d9..bdf8ec1ead 100644 --- a/Terminal.Gui/Input/Responder.cs +++ b/Terminal.Gui/Input/Responder.cs @@ -18,286 +18,182 @@ using System.Linq; using System.Reflection; -namespace Terminal.Gui { - /// - /// Responder base class implemented by objects that want to participate on keyboard and mouse input. - /// - public class Responder : IDisposable { - bool disposedValue; +namespace Terminal.Gui; +/// +/// Responder base class implemented by objects that want to participate on keyboard and mouse input. +/// +public class Responder : IDisposable { + bool disposedValue; #if DEBUG_IDISPOSABLE - /// - /// For debug purposes to verify objects are being disposed properly - /// - public bool WasDisposed = false; - /// - /// For debug purposes to verify objects are being disposed properly - /// - public int DisposedCount = 0; - /// - /// For debug purposes - /// - public static List Instances = new List (); - /// - /// For debug purposes - /// - public Responder () - { - Instances.Add (this); - } + /// + /// For debug purposes to verify objects are being disposed properly + /// + public bool WasDisposed = false; + /// + /// For debug purposes to verify objects are being disposed properly + /// + public int DisposedCount = 0; + /// + /// For debug purposes + /// + public static List Instances = new List (); + /// + /// For debug purposes + /// + public Responder () + { + Instances.Add (this); + } #endif - /// - /// Gets or sets a value indicating whether this can focus. - /// - /// true if can focus; otherwise, false. - public virtual bool CanFocus { get; set; } - - /// - /// Gets or sets a value indicating whether this has focus. - /// - /// true if has focus; otherwise, false. - public virtual bool HasFocus { get; } - - /// - /// Gets or sets a value indicating whether this can respond to user interaction. - /// - public virtual bool Enabled { get; set; } = true; + /// + /// Gets or sets a value indicating whether this can focus. + /// + /// true if can focus; otherwise, false. + public virtual bool CanFocus { get; set; } - /// - /// Gets or sets a value indicating whether this and all its child controls are displayed. - /// - public virtual bool Visible { get; set; } = true; + /// + /// Gets or sets a value indicating whether this has focus. + /// + /// true if has focus; otherwise, false. + public virtual bool HasFocus { get; } - // Key handling - /// - /// This method can be overwritten by view that - /// want to provide accelerator functionality - /// (Alt-key for example). - /// - /// - /// - /// Before keys are sent to the subview on the - /// current view, all the views are - /// processed and the key is passed to the widgets - /// to allow some of them to process the keystroke - /// as a hot-key. - /// - /// For example, if you implement a button that - /// has a hotkey ok "o", you would catch the - /// combination Alt-o here. If the event is - /// caught, you must return true to stop the - /// keystroke from being dispatched to other - /// views. - /// - /// + /// + /// Gets or sets a value indicating whether this can respond to user interaction. + /// + public virtual bool Enabled { get; set; } = true; - public virtual bool ProcessHotKey (KeyEvent kb) - { - return false; - } + /// + /// Gets or sets a value indicating whether this and all its child controls are displayed. + /// + public virtual bool Visible { get; set; } = true; - /// - /// If the view is focused, gives the view a - /// chance to process the keystroke. - /// - /// - /// - /// Views can override this method if they are - /// interested in processing the given keystroke. - /// If they consume the keystroke, they must - /// return true to stop the keystroke from being - /// processed by other widgets or consumed by the - /// widget engine. If they return false, the - /// keystroke will be passed using the ProcessColdKey - /// method to other views to process. - /// - /// - /// The View implementation does nothing but return false, - /// so it is not necessary to call base.ProcessKey if you - /// derive directly from View, but you should if you derive - /// other View subclasses. - /// - /// - /// Contains the details about the key that produced the event. - public virtual bool ProcessKey (KeyEvent keyEvent) - { - return false; - } + /// + /// Method invoked when a mouse event is generated + /// + /// true, if the event was handled, false otherwise. + /// Contains the details about the mouse event. + public virtual bool MouseEvent (MouseEvent mouseEvent) + { + return false; + } - /// - /// This method can be overwritten by views that - /// want to provide accelerator functionality - /// (Alt-key for example), but without - /// interefering with normal ProcessKey behavior. - /// - /// - /// - /// After keys are sent to the subviews on the - /// current view, all the view are - /// processed and the key is passed to the views - /// to allow some of them to process the keystroke - /// as a cold-key. - /// - /// This functionality is used, for example, by - /// default buttons to act on the enter key. - /// Processing this as a hot-key would prevent - /// non-default buttons from consuming the enter - /// keypress when they have the focus. - /// - /// - /// Contains the details about the key that produced the event. - public virtual bool ProcessColdKey (KeyEvent keyEvent) - { - return false; - } + /// + /// Called when the mouse first enters the view; the view will now + /// receives mouse events until the mouse leaves the view. At which time, + /// will be called. + /// + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnMouseEnter (MouseEvent mouseEvent) + { + return false; + } - /// - /// Method invoked when a key is pressed. - /// - /// Contains the details about the key that produced the event. - /// true if the event was handled - public virtual bool OnKeyDown (KeyEvent keyEvent) - { - return false; - } + /// + /// Called when the mouse has moved outside of the view; the view will no longer receive mouse events (until + /// the mouse moves within the view again and is called). + /// + /// + /// true, if the event was handled, false otherwise. + public virtual bool OnMouseLeave (MouseEvent mouseEvent) + { + return false; + } - /// - /// Method invoked when a key is released. - /// - /// Contains the details about the key that produced the event. - /// true if the event was handled - public virtual bool OnKeyUp (KeyEvent keyEvent) - { - return false; - } + /// + /// Method invoked when a view gets focus. + /// + /// The view that is losing focus. + /// true, if the event was handled, false otherwise. + public virtual bool OnEnter (View view) + { + return false; + } - /// - /// Method invoked when a mouse event is generated - /// - /// true, if the event was handled, false otherwise. - /// Contains the details about the mouse event. - public virtual bool MouseEvent (MouseEvent mouseEvent) - { - return false; - } + /// + /// Method invoked when a view loses focus. + /// + /// The view that is getting focus. + /// true, if the event was handled, false otherwise. + public virtual bool OnLeave (View view) + { + return false; + } - /// - /// Called when the mouse first enters the view; the view will now - /// receives mouse events until the mouse leaves the view. At which time, - /// will be called. - /// - /// - /// true, if the event was handled, false otherwise. - public virtual bool OnMouseEnter (MouseEvent mouseEvent) - { - return false; - } + /// + /// Method invoked when the property from a view is changed. + /// + public virtual void OnCanFocusChanged () { } - /// - /// Called when the mouse has moved outside of the view; the view will no longer receive mouse events (until - /// the mouse moves within the view again and is called). - /// - /// - /// true, if the event was handled, false otherwise. - public virtual bool OnMouseLeave (MouseEvent mouseEvent) - { - return false; - } + /// + /// Method invoked when the property from a view is changed. + /// + public virtual void OnEnabledChanged () { } - /// - /// Method invoked when a view gets focus. - /// - /// The view that is losing focus. - /// true, if the event was handled, false otherwise. - public virtual bool OnEnter (View view) - { - return false; - } + /// + /// Method invoked when the property from a view is changed. + /// + public virtual void OnVisibleChanged () { } - /// - /// Method invoked when a view loses focus. - /// - /// The view that is getting focus. - /// true, if the event was handled, false otherwise. - public virtual bool OnLeave (View view) - { + // TODO: v2 - nuke this + /// + /// Utilty function to determine is overridden in the . + /// + /// The view. + /// The method name. + /// if it's overridden, otherwise. + internal static bool IsOverridden (Responder subclass, string method) + { + MethodInfo m = subclass.GetType ().GetMethod (method, + BindingFlags.Instance + | BindingFlags.Public + | BindingFlags.NonPublic + | BindingFlags.DeclaredOnly); + if (m == null) { return false; } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; + } - /// - /// Method invoked when the property from a view is changed. - /// - public virtual void OnCanFocusChanged () { } - - /// - /// Method invoked when the property from a view is changed. - /// - public virtual void OnEnabledChanged () { } - - /// - /// Method invoked when the property from a view is changed. - /// - public virtual void OnVisibleChanged () { } - - // TODO: v2 - nuke this - /// - /// Utilty function to determine is overridden in the . - /// - /// The view. - /// The method name. - /// if it's overridden, otherwise. - internal static bool IsOverridden (Responder subclass, string method) - { - MethodInfo m = subclass.GetType ().GetMethod (method, - BindingFlags.Instance - | BindingFlags.Public - | BindingFlags.NonPublic - | BindingFlags.DeclaredOnly); - if (m == null) { - return false; + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + /// + /// If disposing equals true, the method has been called directly + /// or indirectly by a user's code. Managed and unmanaged resources + /// can be disposed. + /// If disposing equals false, the method has been called by the + /// runtime from inside the finalizer and you should not reference + /// other objects. Only unmanaged resources can be disposed. + /// + /// + protected virtual void Dispose (bool disposing) + { + if (!disposedValue) { + if (disposing) { + // TODO: dispose managed state (managed objects) } - return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; - } - - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. - /// - /// - /// If disposing equals true, the method has been called directly - /// or indirectly by a user's code. Managed and unmanaged resources - /// can be disposed. - /// If disposing equals false, the method has been called by the - /// runtime from inside the finalizer and you should not reference - /// other objects. Only unmanaged resources can be disposed. - /// - /// - protected virtual void Dispose (bool disposing) - { - if (!disposedValue) { - if (disposing) { - // TODO: dispose managed state (managed objects) - } - disposedValue = true; - } + disposedValue = true; } + } - /// - /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource. - /// - public void Dispose () - { - // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method - Dispose (disposing: true); - GC.SuppressFinalize (this); + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resource. + /// + public void Dispose () + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose (disposing: true); + GC.SuppressFinalize (this); #if DEBUG_IDISPOSABLE - WasDisposed = true; + WasDisposed = true; - foreach (var instance in Instances.Where (x => x.WasDisposed).ToList ()) { - Instances.Remove (instance); - } -#endif + foreach (var instance in Instances.Where (x => x.WasDisposed).ToList ()) { + Instances.Remove (instance); } +#endif } } diff --git a/Terminal.Gui/Input/ShortcutHelper.cs b/Terminal.Gui/Input/ShortcutHelper.cs index e1b1a3bdbb..4d02f41284 100644 --- a/Terminal.Gui/Input/ShortcutHelper.cs +++ b/Terminal.Gui/Input/ShortcutHelper.cs @@ -1,270 +1,152 @@ using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Text; -using System.Threading.Tasks; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Represents a helper to manipulate shortcut keys used on views. +/// +public class ShortcutHelper { + // TODO: Update this to use Key, not KeyCode + private KeyCode shortcut; + /// - /// Represents a helper to manipulate shortcut keys used on views. + /// This is the global setting that can be used as a global shortcut to invoke the action on the view. /// - public class ShortcutHelper { - private Key shortcut; - - /// - /// This is the global setting that can be used as a global shortcut to invoke the action on the view. - /// - public virtual Key Shortcut { - get => shortcut; - set { - if (shortcut != value && (PostShortcutValidation (value) || value == Key.Null)) { - shortcut = value; - } + public virtual KeyCode Shortcut { + get => shortcut; + set { + if (shortcut != value && (PostShortcutValidation (value) || value is KeyCode.Null or KeyCode.Unknown)) { + shortcut = value; } } + } - /// - /// The keystroke combination used in the as string. - /// - public virtual string ShortcutTag => GetShortcutTag (shortcut); - - /// - /// The action to run if the is defined. - /// - public virtual Action ShortcutAction { get; set; } - - /// - /// Gets the key with all the keys modifiers, especially the shift key that sometimes have to be injected later. - /// - /// The to check. - /// The with all the keys modifiers. - public static Key GetModifiersKey (KeyEvent kb) - { - var key = kb.Key; - if (kb.IsAlt && (key & Key.AltMask) == 0) { - key |= Key.AltMask; - } - if (kb.IsCtrl && (key & Key.CtrlMask) == 0) { - key |= Key.CtrlMask; - } - if (kb.IsShift && (key & Key.ShiftMask) == 0) { - key |= Key.ShiftMask; - } - - return key; + /// + /// The keystroke combination used in the as string. + /// + public virtual string ShortcutTag => Key.ToString (shortcut, MenuBar.ShortcutDelimiter); + + /// + /// Return key as string. + /// + /// The key to extract. + /// Correspond to the non modifier key. + static string GetKeyToString (KeyCode key, out KeyCode knm) + { + if (key == KeyCode.Null) { + knm = KeyCode.Null; + return ""; } - /// - /// Get the key as string. - /// - /// The shortcut key. - /// The delimiter string. - /// - public static string GetShortcutTag (Key shortcut, string delimiter = null) - { - if (shortcut == Key.Null) { - return ""; - } - - var k = shortcut; - if (delimiter == null) { - delimiter = MenuBar.ShortcutDelimiter; - } - string tag = string.Empty; - var sCut = GetKeyToString (k, out Key knm).ToString (); - if (knm == Key.Unknown) { - k &= ~Key.Unknown; - sCut = GetKeyToString (k, out _).ToString (); - } - if ((k & Key.CtrlMask) != 0) { - tag = "Ctrl"; + knm = key; + var mK = key & (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.ShiftMask); + knm &= ~mK; + for (uint i = (uint)KeyCode.F1; i < (uint)KeyCode.F12; i++) { + if (knm == (KeyCode)i) { + mK |= (KeyCode)i; } - if ((k & Key.ShiftMask) != 0) { - if (!string.IsNullOrEmpty(tag)) { - tag += delimiter; - } - tag += "Shift"; - } - if ((k & Key.AltMask) != 0) { - if (!string.IsNullOrEmpty(tag)) { - tag += delimiter; - } - tag += "Alt"; - } - - string [] keys = sCut.Split (","); - for (int i = 0; i < keys.Length; i++) { - var key = keys [i].Trim (); - if (key == Key.AltMask.ToString () || key == Key.ShiftMask.ToString () || key == Key.CtrlMask.ToString ()) { - continue; - } - if (!string.IsNullOrEmpty(tag)) { - tag += delimiter; - } - if (!key.Contains ("F") && key.Length > 2 && keys.Length == 1) { - k = (uint)Key.AltMask + k; - tag += ((char)k).ToString (); - } else if (key.Length == 2 && key.StartsWith ("D")) { - tag += ((char)key.ElementAt (1)).ToString (); - } else { - tag += key; - } - } - - return tag; } - - /// - /// Return key as string. - /// - /// The key to extract. - /// Correspond to the non modifier key. - public static string GetKeyToString (Key key, out Key knm) - { - if (key == Key.Null) { - knm = Key.Null; - return ""; - } - - knm = key; - var mK = key & (Key.AltMask | Key.CtrlMask | Key.ShiftMask); - knm &= ~mK; - for (uint i = (uint)Key.F1; i < (uint)Key.F12; i++) { - if (knm == (Key)i) { - mK |= (Key)i; - } - } - knm &= ~mK; - uint.TryParse (knm.ToString (), out uint c); - var s = mK == Key.Null ? "" : mK.ToString (); - if (s != "" && (knm != Key.Null || c > 0)) { - s += ","; - } - s += c == 0 ? knm == Key.Null ? "" : knm.ToString () : ((char)c).ToString (); - return s; + knm &= ~mK; + uint.TryParse (knm.ToString (), out uint c); + var s = mK == KeyCode.Null ? "" : mK.ToString (); + if (s != "" && (knm != KeyCode.Null || c > 0)) { + s += ","; } + s += c == 0 ? knm == KeyCode.Null ? "" : knm.ToString () : ((char)c).ToString (); + return s; + } - /// - /// Allows to retrieve a from a - /// - /// The key as string. - /// The delimiter string. - public static Key GetShortcutFromTag (string tag, string delimiter = null) - { - var sCut = tag; - if (string.IsNullOrEmpty(sCut)) { - return default; - } + /// + /// Allows to retrieve a from a + /// + /// The key as string. + /// The delimiter string. + public static KeyCode GetShortcutFromTag (string tag, Rune delimiter = default) + { + var sCut = tag; + if (string.IsNullOrEmpty (sCut)) { + return default; + } - Key key = Key.Null; - //var hasCtrl = false; - if (delimiter == null) { - delimiter = MenuBar.ShortcutDelimiter; - } + KeyCode key = KeyCode.Null; + //var hasCtrl = false; + if (delimiter == default) { + delimiter = MenuBar.ShortcutDelimiter; + } - string [] keys = sCut.Split (delimiter); - for (int i = 0; i < keys.Length; i++) { - var k = keys [i]; - if (k == "Ctrl") { - //hasCtrl = true; - key |= Key.CtrlMask; - } else if (k == "Shift") { - key |= Key.ShiftMask; - } else if (k == "Alt") { - key |= Key.AltMask; - } else if (k.StartsWith ("F") && k.Length > 1) { - int.TryParse (k.Substring (1).ToString (), out int n); - for (uint j = (uint)Key.F1; j <= (uint)Key.F12; j++) { - int.TryParse (((Key)j).ToString ().Substring (1), out int f); - if (f == n) { - key |= (Key)j; - } + string [] keys = sCut.Split (delimiter.ToString()); + for (int i = 0; i < keys.Length; i++) { + var k = keys [i]; + if (k == "Ctrl") { + //hasCtrl = true; + key |= KeyCode.CtrlMask; + } else if (k == "Shift") { + key |= KeyCode.ShiftMask; + } else if (k == "Alt") { + key |= KeyCode.AltMask; + } else if (k.StartsWith ("F") && k.Length > 1) { + int.TryParse (k.Substring (1).ToString (), out int n); + for (uint j = (uint)KeyCode.F1; j <= (uint)KeyCode.F12; j++) { + int.TryParse (((KeyCode)j).ToString ().Substring (1), out int f); + if (f == n) { + key |= (KeyCode)j; } - } else { - key |= (Key)Enum.Parse (typeof (Key), k.ToString ()); } + } else { + key |= (KeyCode)Enum.Parse (typeof (KeyCode), k.ToString ()); } - - return key; } - /// - /// Lookup for a on range of keys. - /// - /// The source key. - /// First key in range. - /// Last key in range. - public static bool CheckKeysFlagRange (Key key, Key first, Key last) - { - for (uint i = (uint)first; i < (uint)last; i++) { - if ((key | (Key)i) == key) { - return true; - } - } - return false; - } + return key; + } - /// - /// Used at key down or key press validation. - /// - /// The key to validate. - /// true if is valid.falseotherwise. - public static bool PreShortcutValidation (Key key) - { - if ((key & (Key.CtrlMask | Key.ShiftMask | Key.AltMask)) == 0 && !CheckKeysFlagRange (key, Key.F1, Key.F12)) { - return false; + /// + /// Lookup for a on range of keys. + /// + /// The source key. + /// First key in range. + /// Last key in range. + public static bool CheckKeysFlagRange (KeyCode key, KeyCode first, KeyCode last) + { + for (uint i = (uint)first; i < (uint)last; i++) { + if ((key | (KeyCode)i) == key) { + return true; } - - return true; } + return false; + } - /// - /// Used at key up validation. - /// - /// The key to validate. - /// true if is valid.falseotherwise. - public static bool PostShortcutValidation (Key key) - { - GetKeyToString (key, out Key knm); - - if (CheckKeysFlagRange (key, Key.F1, Key.F12) || - ((key & (Key.CtrlMask | Key.ShiftMask | Key.AltMask)) != 0 && knm != Key.Null && knm != Key.Unknown)) { - return true; - } + /// + /// Used at key down or key press validation. + /// + /// The key to validate. + /// true if is valid.falseotherwise. + public static bool PreShortcutValidation (KeyCode key) + { + if ((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) == 0 && !CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12)) { return false; } - /// - /// Allows a view to run a if defined. - /// - /// The - /// The - /// true if defined falseotherwise. - public static bool FindAndOpenByShortcut (KeyEvent kb, View view = null) - { - if (view == null) { - return false; } - - var key = kb.KeyValue; - var keys = GetModifiersKey (kb); - key |= (int)keys; - foreach (var v in view.Subviews) { - if (v.Shortcut != Key.Null && v.Shortcut == (Key)key) { - var action = v.ShortcutAction; - if (action != null) { - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } - return true; - } - if (FindAndOpenByShortcut (kb, v)) { - return true; - } - } + return true; + } - return false; + /// + /// Used at key up validation. + /// + /// The key to validate. + /// true if is valid.falseotherwise. + public static bool PostShortcutValidation (KeyCode key) + { + GetKeyToString (key, out KeyCode knm); + + if (CheckKeysFlagRange (key, KeyCode.F1, KeyCode.F12) || + ((key & (KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask)) != 0 && knm != KeyCode.Null && knm != KeyCode.Unknown)) { + return true; } + Debug.WriteLine ($"WARNING: {Key.ToString (key)} is not a valid shortcut key."); + return false; } } + diff --git a/Terminal.Gui/MainLoop.cs b/Terminal.Gui/MainLoop.cs index 78275887fc..c266abbc28 100644 --- a/Terminal.Gui/MainLoop.cs +++ b/Terminal.Gui/MainLoop.cs @@ -10,7 +10,7 @@ namespace Terminal.Gui { /// - /// Public interface to create a platform specific driver. + /// Interface to create a platform specific driver. /// internal interface IMainLoopDriver { /// @@ -295,7 +295,7 @@ void RunIdle () /// /// Used for unit tests. /// - internal bool Running { get; private set; } + internal bool Running { get; set; } /// /// Determines whether there are pending events to be processed. diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 67afbf4aee..af2c93e197 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -17,22 +17,13 @@ "ConfigurationManager.ThrowOnJsonErrors": false, "Application.AlternateBackwardKey": { - "Key": "PageUp", - "Modifiers": [ - "Ctrl" - ] + "Key": "Ctrl+PageUp" }, "Application.AlternateForwardKey": { - "Key": "PageDown", - "Modifiers": [ - "Ctrl" - ] + "Key": "Ctrl+PageDown" }, "Application.QuitKey": { - "Key": "Q", - "Modifiers": [ - "Ctrl" - ] + "Key": "Ctrl+Q" }, "Application.IsMouseDisabled": false, "Theme": "Default", diff --git a/Terminal.Gui/Terminal.Gui.csproj b/Terminal.Gui/Terminal.Gui.csproj index a71ea78d7b..a73a949234 100644 --- a/Terminal.Gui/Terminal.Gui.csproj +++ b/Terminal.Gui/Terminal.Gui.csproj @@ -19,8 +19,8 @@ portable - net7.0 - 11.0 + net8.0 + Terminal.Gui Terminal.Gui true @@ -38,9 +38,8 @@ - - + diff --git a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs index 9aba845ede..9f8392d676 100644 --- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs @@ -29,7 +29,7 @@ public class AppendAutocomplete : AutocompleteBase { public AppendAutocomplete (TextField textField) { this.textField = textField; - SelectionKey = Key.Tab; + SelectionKey = KeyCode.Tab; ColorScheme = new ColorScheme { Normal = new Attribute (Color.DarkGray, Color.Black), @@ -54,16 +54,16 @@ public override bool MouseEvent (MouseEvent me, bool fromHost = false) } /// - public override bool ProcessKey (KeyEvent kb) + public override bool ProcessKey (Key a) { - var key = kb.Key; + var key = a.KeyCode; if (key == SelectionKey) { return this.AcceptSelectionIfAny (); } else - if (key == Key.CursorUp) { + if (key == KeyCode.CursorUp) { return this.CycleSuggestion (1); } else - if (key == Key.CursorDown) { + if (key == KeyCode.CursorDown) { return this.CycleSuggestion (-1); } else if (key == CloseKey && Suggestions.Any ()) { ClearSuggestions (); @@ -71,7 +71,7 @@ public override bool ProcessKey (KeyEvent kb) return true; } - if (char.IsLetterOrDigit ((char)kb.KeyValue)) { + if (char.IsLetterOrDigit ((char)a)) { _suspendSuggestions = false; } diff --git a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs index 6be3e8da00..524c7a2433 100644 --- a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs +++ b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs @@ -39,14 +39,17 @@ public abstract class AutocompleteBase : IAutocomplete { /// public abstract ColorScheme ColorScheme { get; set; } + // TODO: Update to use Key instead of KeyCode /// - public virtual Key SelectionKey { get; set; } = Key.Enter; + public virtual KeyCode SelectionKey { get; set; } = KeyCode.Enter; + // TODO: Update to use Key instead of KeyCode /// - public virtual Key CloseKey { get; set; } = Key.Esc; + public virtual KeyCode CloseKey { get; set; } = KeyCode.Esc; + // TODO: Update to use Key instead of KeyCode /// - public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask; + public virtual KeyCode Reopen { get; set; } = KeyCode.Space | KeyCode.CtrlMask | KeyCode.AltMask; /// public virtual AutocompleteContext Context { get; set; } @@ -55,7 +58,7 @@ public abstract class AutocompleteBase : IAutocomplete { public abstract bool MouseEvent (MouseEvent me, bool fromHost = false); /// - public abstract bool ProcessKey (KeyEvent kb); + public abstract bool ProcessKey (Key a); /// public abstract void RenderOverlay (Point renderAt); diff --git a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs index ac31d3f9c7..ca57dda536 100644 --- a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs @@ -53,20 +53,23 @@ public interface IAutocomplete { /// ColorScheme ColorScheme { get; set; } + // TODO: Update to use Key instead of KeyCode /// /// The key that the user must press to accept the currently selected autocomplete suggestion /// - Key SelectionKey { get; set; } + KeyCode SelectionKey { get; set; } + // TODO: Update to use Key instead of KeyCode /// /// The key that the user can press to close the currently popped autocomplete menu /// - Key CloseKey { get; set; } + KeyCode CloseKey { get; set; } + // TODO: Update to use Key instead of KeyCode /// /// The key that the user can press to reopen the currently popped autocomplete menu /// - Key Reopen { get; set; } + KeyCode Reopen { get; set; } /// /// The context used by the autocomplete menu. @@ -85,9 +88,9 @@ public interface IAutocomplete { /// up/down apply to the autocomplete control instead of changing the cursor position in /// the underlying text view. /// - /// The key event. + /// The key event. /// trueif the key can be handled falseotherwise. - bool ProcessKey (KeyEvent kb); + bool ProcessKey (Key a); /// /// Handle mouse events before e.g. to make mouse events like diff --git a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs index 925f5bc53b..d78767a469 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -152,7 +152,7 @@ public override ColorScheme ColorScheme { public override void RenderOverlay (Point renderAt) { if (!Context.Canceled && Suggestions.Count > 0 && !Visible && HostControl?.HasFocus == true) { - ProcessKey (new KeyEvent ((Key)(Suggestions [0].Title [0]), new KeyModifiers ())); + ProcessKey (new ((KeyCode)(Suggestions [0].Title [0]))); } else if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) { LastPopupPos = null; Visible = false; @@ -276,18 +276,18 @@ public override void EnsureSelectedIdxIsValid () /// up/down apply to the autocomplete control instead of changing the cursor position in /// the underlying text view. /// - /// The key event. + /// The key event. /// trueif the key can be handled falseotherwise. - public override bool ProcessKey (KeyEvent kb) + public override bool ProcessKey (Key a) { - if (SuggestionGenerator.IsWordChar ((Rune)(char)kb.Key)) { + if (SuggestionGenerator.IsWordChar ((Rune)a)) { Visible = true; ManipulatePopup (); closed = false; return false; } - if (kb.Key == Reopen) { + if (a.KeyCode == Reopen) { Context.Canceled = false; return ReopenSuggestions (); } @@ -300,19 +300,19 @@ public override bool ProcessKey (KeyEvent kb) return false; } - if (kb.Key == Key.CursorDown) { + if (a.KeyCode == KeyCode.CursorDown) { MoveDown (); return true; } - if (kb.Key == Key.CursorUp) { + if (a.KeyCode == KeyCode.CursorUp) { MoveUp (); return true; } // TODO : Revisit this - /*if (kb.Key == Key.CursorLeft || kb.Key == Key.CursorRight) { - GenerateSuggestions (kb.Key == Key.CursorLeft ? -1 : 1); + /*if (a.ConsoleDriverKey == Key.CursorLeft || a.ConsoleDriverKey == Key.CursorRight) { + GenerateSuggestions (a.ConsoleDriverKey == Key.CursorLeft ? -1 : 1); if (Suggestions.Count == 0) { Visible = false; if (!closed) { @@ -322,11 +322,11 @@ public override bool ProcessKey (KeyEvent kb) return false; }*/ - if (kb.Key == SelectionKey) { + if (a.KeyCode == SelectionKey) { return Select (); } - if (kb.Key == CloseKey) { + if (a.KeyCode == CloseKey) { Close (); Context.Canceled = true; return true; diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index 1d65116dc6..64161ebca5 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -203,15 +203,15 @@ private void ClearSearchString () } /// - /// Returns true if is a searchable key + /// Returns true if is a searchable key /// (e.g. letters, numbers, etc) that are valid to pass to this /// class for search filtering. /// - /// + /// /// - public static bool IsCompatibleKey (KeyEvent kb) + public static bool IsCompatibleKey (Key a) { - return !kb.IsAlt && !kb.IsCtrl; + return !a.IsAlt && !a.IsCtrl; } } } diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 1b38430bba..f332b52a3e 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -929,20 +929,20 @@ public static Rect CalcRect (int x, int y, string text, TextDirection direction } /// - /// Finds the hotkey and its location in text. + /// Finds the HotKey and its location in text. /// /// The text to look in. - /// The hotkey specifier (e.g. '_') to look for. - /// If true the legacy behavior of identifying the first upper case character as the hotkey will be enabled. + /// The HotKey specifier (e.g. '_') to look for. + /// If true the legacy behavior of identifying the first upper case character as the HotKey will be enabled. /// Regardless of the value of this parameter, hotKeySpecifier takes precedence. /// Outputs the Rune index into text. - /// Outputs the hotKey. - /// true if a hotkey was found; false otherwise. + /// Outputs the hotKey. if not found. + /// true if a HotKey was found; false otherwise. public static bool FindHotKey (string text, Rune hotKeySpecifier, bool firstUpperCase, out int hotPos, out Key hotKey) { if (string.IsNullOrEmpty (text) || hotKeySpecifier == (Rune)0xFFFF) { hotPos = -1; - hotKey = Key.Unknown; + hotKey = KeyCode.Null; return false; } @@ -983,14 +983,18 @@ public static bool FindHotKey (string text, Rune hotKeySpecifier, bool firstUppe if (hot_key != (Rune)0 && hot_pos != -1) { hotPos = hot_pos; - if (Rune.IsValid (hot_key.Value) && char.IsLetterOrDigit ((char)hot_key.Value)) { - hotKey = (Key)char.ToUpperInvariant ((char)hot_key.Value); + var newHotKey = (KeyCode)hot_key.Value; + if (newHotKey != KeyCode.Unknown && newHotKey != KeyCode.Null && !(newHotKey == KeyCode.Space || Rune.IsControl (hot_key))) { + if ((newHotKey & ~KeyCode.Space) is >= KeyCode.A and <= KeyCode.Z) { + newHotKey &= ~KeyCode.Space; + } + hotKey = newHotKey; return true; } } hotPos = -1; - hotKey = Key.Unknown; + hotKey = KeyCode.Null; return false; } @@ -1047,7 +1051,7 @@ public static string RemoveHotKeySpecifier (string text, int hotPos, Rune hotKey TextAlignment _textAlignment; VerticalTextAlignment _textVerticalAlignment; TextDirection _textDirection; - Key _hotKey; + Key _hotKey = new Key (); int _hotKeyPos = -1; Size _size; private bool _autoSize; @@ -1225,17 +1229,17 @@ public Size Size { } /// - /// The specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. + /// The specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable hot key support for this View instance. The default is '\xffff'. /// public Rune HotKeySpecifier { get; set; } = (Rune)0xFFFF; /// - /// The position in the text of the hotkey. The hotkey will be rendered using the hot color. + /// The position in the text of the hot key. The hot key will be rendered using the hot color. /// public int HotKeyPos { get => _hotKeyPos; internal set => _hotKeyPos = value; } /// - /// Gets the hotkey. Will be an upper case letter or digit. + /// Gets or sets the hot key. Must be be an upper case letter or digit. Fires the event. /// public Key HotKey { get => _hotKey; @@ -1287,10 +1291,10 @@ public List Lines { NeedsFormat = false; return _lines; } - + if (NeedsFormat) { var shown_text = _text; - if (FindHotKey (_text, HotKeySpecifier, true, out _hotKeyPos, out Key newHotKey)) { + if (FindHotKey (_text, HotKeySpecifier, true, out _hotKeyPos, out var newHotKey)) { HotKey = newHotKey; shown_text = RemoveHotKeySpecifier (Text, _hotKeyPos, HotKeySpecifier); shown_text = ReplaceHotKeyWithTag (shown_text, _hotKeyPos); diff --git a/Terminal.Gui/Text/ViewLayout.cs b/Terminal.Gui/Text/ViewLayout.cs index 4109758abd..faeb8e1740 100644 --- a/Terminal.Gui/Text/ViewLayout.cs +++ b/Terminal.Gui/Text/ViewLayout.cs @@ -534,7 +534,7 @@ internal void SetNeedsLayout () } /// - /// Removes the setting on this view. + /// Indicates that the view does not need to be laid out. /// protected void ClearLayoutNeeded () { @@ -947,7 +947,7 @@ internal virtual void LayoutFrames () /// response to the container view or terminal resizing. /// /// - /// Calls (which raises the event) before it returns. + /// Raises the event) before it returns. /// public virtual void LayoutSubviews () { diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 66799dc50d..aa23cf47ce 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -8,22 +8,24 @@ namespace Terminal.Gui { #region API Docs /// /// View is the base class for all views on the screen and represents a visible element that can render itself and - /// contains zero or more nested views, called SubViews. + /// contains zero or more nested views, called SubViews. View provides basic functionality for layout, positioning, + /// and drawing. In addition, View provides keyboard and mouse event handling. /// /// + /// + /// + /// TermDefinition + /// + /// + /// SubViewA View that is contained in another view and will be rendered as part of the containing view's ContentArea. + /// SubViews are added to another view via the ` method. A View may only be a SubView of a single View. + /// + /// + /// SuperViewThe View that is a container for SubViews. + /// + /// /// - /// The View defines the base functionality for user interface elements in Terminal.Gui. Views - /// can contain one or more subviews, can respond to user input and render themselves on the screen. - /// - /// - /// SubView - A View that is contained in another view and will be rendered as part of the containing view's ContentArea. - /// SubViews are added to another view via the ` method. A View may only be a SubView of a single View. - /// - /// - /// SuperView - The View that is a container for SubViews. - /// - /// - /// Focus is a concept that is used to describe which Responder is currently receiving user input. Only views that are + /// Focus is a concept that is used to describe which View is currently receiving user input. Only Views that are /// , , and will receive focus. /// /// @@ -110,7 +112,9 @@ namespace Terminal.Gui { /// to override base class layout code optimally by doing so only on first run, /// instead of on every run. /// - /// + /// + /// See for an overview of View keyboard handling. + /// /// #endregion API Docs public partial class View : Responder, ISupportInitializeNotification { @@ -220,7 +224,6 @@ void SetInitialProperties (string text, Rect rect, LayoutStyle layoutStyle = Lay TextFormatter.HotKeyChanged += TextFormatter_HotKeyChanged; TextDirection = direction; - _shortcutHelper = new ShortcutHelper (); CanFocus = false; TabIndex = -1; TabStop = false; @@ -231,6 +234,8 @@ void SetInitialProperties (string text, Rect rect, LayoutStyle layoutStyle = Lay Frame = rect.IsEmpty ? TextFormatter.CalcRect (0, 0, text, direction) : rect; OnResizeNeeded (); + AddCommands (); + CreateFrames (); LayoutFrames (); diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index 833691495a..4e5ed8206e 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -200,6 +200,9 @@ public void SetSubViewNeedsDisplay () /// The screen-relative rectangle to clear. public void Clear (Rect regionScreen) { + if (Driver == null) { + return; + } var prev = Driver.SetAttribute (GetNormalColor ()); Driver.FillRect (regionScreen); Driver.SetAttribute (prev); diff --git a/Terminal.Gui/View/ViewEventArgs.cs b/Terminal.Gui/View/ViewEventArgs.cs index 576d5e6acf..e56759fe04 100644 --- a/Terminal.Gui/View/ViewEventArgs.cs +++ b/Terminal.Gui/View/ViewEventArgs.cs @@ -63,7 +63,7 @@ public DrawEventArgs (Rect rect) } /// - /// Defines the event arguments for + /// Defines the event arguments for /// public class FocusEventArgs : EventArgs { /// diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs index 421f941621..d1c4e034c0 100644 --- a/Terminal.Gui/View/ViewKeyboard.cs +++ b/Terminal.Gui/View/ViewKeyboard.cs @@ -1,464 +1,685 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Text; -namespace Terminal.Gui { - public partial class View { - ShortcutHelper _shortcutHelper; - - /// - /// Event invoked when the is changed. - /// - public event EventHandler HotKeyChanged; - - Key _hotKey = Key.Null; - - /// - /// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire. - /// - public virtual Key HotKey { - get => _hotKey; - set { - if (_hotKey != value) { - var v = value == Key.Unknown ? Key.Null : value; - if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) { - if (v == Key.Null) { - ClearKeyBinding (Key.Space | _hotKey); - } else { - ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v); - } - } else if (v != Key.Null) { - AddKeyBinding (Key.Space | v, Command.Accept); - } - _hotKey = TextFormatter.HotKey = v; - } +namespace Terminal.Gui; +public partial class View { + + void AddCommands () + { + // By default, the Default command is bound to the HotKey enabling focus + AddCommand (Command.Default, () => { + if (CanFocus) { + SetFocus (); + return true; } - } + return false; + }); - /// - /// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'. - /// - public virtual Rune HotKeySpecifier { - get { - if (TextFormatter != null) { - return TextFormatter.HotKeySpecifier; - } else { - return new Rune ('\xFFFF'); - } + // By default the Accept command does nothing + AddCommand (Command.Accept, () => false); + } + + #region HotKey Support + /// + /// Invoked when the is changed. + /// + public event EventHandler HotKeyChanged; + + Key _hotKey = new Key (); + + void TextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e) + { + HotKeyChanged?.Invoke (this, e); + } + + /// + /// Gets or sets the hot key defined for this view. Pressing the hot key on the keyboard while this view has + /// focus will invoke the and commands. + /// causes the view to be focused and does nothing. + /// By default, the HotKey is automatically set to the first + /// character of that is prefixed with with . + /// + /// A HotKey is a keypress that selects a visible UI item. For selecting items across `s + /// (e.g.a in a ) the keypress must include the modifier. + /// For selecting items within a View that are not Views themselves, the keypress can be key without the Alt modifier. + /// For example, in a Dialog, a Button with the text of "_Text" can be selected with Alt-T. + /// Or, in a with "_File _Edit", Alt-F will select (show) the "_File" menu. + /// If the "_File" menu has a sub-menu of "_New" `Alt-N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened. + /// + /// + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// This is a helper API for configuring a key binding for the hot key. By default, this property is set whenever changes. + /// + /// + /// By default, when the Hot Key is set, key bindings are added for both the base key (e.g. ) and + /// the Alt-shifted key (e.g. | ). + /// This behavior can be overriden by overriding . + /// + /// + /// By default, when the HotKey is set to through key bindings will be added for both the un-shifted and shifted + /// versions. This means if the HotKey is , key bindings for Key.A and Key.A.WithShift + /// will be added. This behavior can be overriden by overriding . + /// + /// + /// If the hot key is changed, the event is fired. + /// + /// + /// Set to to disable the hot key. + /// + /// + public virtual Key HotKey { + get => _hotKey; + set { + if (value is null || value.KeyCode == KeyCode.Unknown) { + throw new ArgumentException (@"HotKey must not be null. Use Key.Empty to clear the HotKey.", nameof (value)); } - set { - TextFormatter.HotKeySpecifier = value; - SetHotKey (); + if (AddKeyBindingsForHotKey (_hotKey, value)) { + // This will cause TextFormatter_HotKeyChanged to be called, firing HotKeyChanged + _hotKey = TextFormatter.HotKey = value; } } + } - /// - /// This is the global setting that can be used as a global shortcut to invoke an action if provided. - /// - public Key Shortcut { - get => _shortcutHelper.Shortcut; - set { - if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == Key.Null)) { - _shortcutHelper.Shortcut = value; - } - } + /// + /// Adds key bindings for the specified HotKey. Useful for views that contain multiple items that each have their own HotKey + /// such as . + /// + /// + /// + /// By default key bindings are added for both the base key (e.g. ) and + /// the Alt-shifted key (e.g. Key.D3.WithAlt + /// This behavior can be overriden by overriding . + /// + /// + /// By default, when is through key bindings will be added for both the un-shifted and shifted + /// versions. This means if the HotKey is , key bindings for Key.A and Key.A.WithShift + /// will be added. This behavior can be overriden by overriding . + /// + /// + /// For each of the bound keys causes the view to be focused and does nothing. + /// + /// + /// The HotKey is replacing. Key bindings for this key will be removed. + /// The new HotKey. If bindings will be removed. + /// if the HotKey bindings were added. + /// + public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey) + { + if ((KeyCode)_hotKey == hotKey) { + return false; } - /// - /// The keystroke combination used in the as string. - /// - public string ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut); - - /// - /// The action to run if the is defined. - /// - public virtual Action ShortcutAction { get; set; } - - // This is null, and allocated on demand. - List _tabIndexes; - - /// - /// Configurable keybindings supported by the control - /// - private Dictionary KeyBindings { get; set; } = new Dictionary (); - private Dictionary> CommandImplementations { get; set; } = new Dictionary> (); - - /// - /// This returns a tab index list of the subviews contained by this view. - /// - /// The tabIndexes. - public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; - - int _tabIndex = -1; - - /// - /// Indicates the index of the current from the list. - /// - public int TabIndex { - get { return _tabIndex; } - set { - if (!CanFocus) { - _tabIndex = -1; - return; - } else if (SuperView?._tabIndexes == null || SuperView?._tabIndexes.Count == 1) { - _tabIndex = 0; - return; - } else if (_tabIndex == value) { - return; - } - _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value; - _tabIndex = GetTabIndex (_tabIndex); - if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) { - SuperView._tabIndexes.Remove (this); - SuperView._tabIndexes.Insert (_tabIndex, this); - SetTabIndex (); - } - } - } + var newKey = hotKey == KeyCode.Unknown ? KeyCode.Null : hotKey; - int GetTabIndex (int idx) - { - var i = 0; - foreach (var v in SuperView._tabIndexes) { - if (v._tabIndex == -1 || v == this) { - continue; - } - i++; - } - return Math.Min (i, idx); + var baseKey = newKey.NoAlt.NoShift.NoCtrl; + if (newKey != Key.Empty && (baseKey == Key.Space || Rune.IsControl (baseKey.AsRune))) { + throw new ArgumentException (@$"HotKey must be a printable (and non-space) key ({hotKey})."); } - void SetTabIndex () - { - var i = 0; - foreach (var v in SuperView._tabIndexes) { - if (v._tabIndex == -1) { - continue; - } - v._tabIndex = i; - i++; + if (newKey != baseKey) { + if (newKey.IsCtrl) { + throw new ArgumentException (@$"HotKey does not support CtrlMask ({hotKey})."); } + // Strip off the shift mask if it's A...Z + if (baseKey.IsKeyCodeAtoZ) { + newKey = newKey.NoShift; + } + // Strip off the Alt mask + newKey = newKey.NoAlt; } - bool _tabStop = true; - - /// - /// This only be if the is also - /// and the focus can be avoided by setting this to - /// - public bool TabStop { - get => _tabStop; - set { - if (_tabStop == value) { - return; - } - _tabStop = CanFocus && value; - } + // Remove base version + if (KeyBindings.TryGet (prevHotKey, out _)) { + KeyBindings.Remove (prevHotKey); } - int _oldTabIndex; - - /// - /// Invoked when a character key is pressed and occurs after the key up event. - /// - public event EventHandler KeyPressed; + // Remove the Alt version + if (KeyBindings.TryGet (prevHotKey.WithAlt, out _)) { + KeyBindings.Remove (prevHotKey.WithAlt); + } - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; + if (_hotKey.KeyCode is >= KeyCode.A and <= KeyCode.Z) { + // Remove the shift version + if (KeyBindings.TryGet (prevHotKey.WithShift, out _)) { + KeyBindings.Remove (prevHotKey.WithShift); } - - var args = new KeyEventEventArgs (keyEvent); - KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; - if (Focused?.Enabled == true) { - Focused?.KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; + // Remove alt | shift version + if (KeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _)) { + KeyBindings.Remove (prevHotKey.WithShift.WithAlt); } - - return Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true; } - /// - /// Invokes any binding that is registered on this - /// and matches the - /// - /// The key event passed. - protected bool? InvokeKeybindings (KeyEvent keyEvent) - { - bool? toReturn = null; - - if (KeyBindings.ContainsKey (keyEvent.Key)) { - - foreach (var command in KeyBindings [keyEvent.Key]) { + // Add the new + if (newKey != KeyCode.Null) { + // Add the base and Alt key + KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.Default, Command.Accept); + KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.Default, Command.Accept); - if (!CommandImplementations.ContainsKey (command)) { - throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})"); - } - - // each command has its own return value - var thisReturn = CommandImplementations [command] (); + // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask + if (newKey.IsKeyCodeAtoZ) { + KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.Default, Command.Accept); + KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.Default, Command.Accept); + } + } + return true; + } - // if we haven't got anything yet, the current command result should be used - if (toReturn == null) { - toReturn = thisReturn; - } - // if ever see a true then that's what we will return - if (thisReturn ?? false) { - toReturn = true; - } - } + /// + /// Gets or sets the specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable automatic hot key setting + /// support for this View instance. The default is '\xffff'. + /// + public virtual Rune HotKeySpecifier { + get { + if (TextFormatter != null) { + return TextFormatter.HotKeySpecifier; + } else { + return new Rune ('\xFFFF'); } + } + set { + TextFormatter.HotKeySpecifier = value; + SetHotKey (); + } + } - return toReturn; - } - - /// - /// Adds a new key combination that will trigger the given - /// (if supported by the View - see ) - /// - /// If the key is already bound to a different it will be - /// rebound to this one - /// Commands are only ever applied to the current (i.e. this feature - /// cannot be used to switch focus to another view and perform multiple commands there) - /// - /// - /// The command(s) to run on the when is pressed. - /// When specifying multiple commands, all commands will be applied in sequence. The bound strike - /// will be consumed if any took effect. - public void AddKeyBinding (Key key, params Command [] command) - { - if (command.Length == 0) { - throw new ArgumentException ("At least one command must be specified", nameof (command)); + void SetHotKey () + { + if (TextFormatter == null || HotKeySpecifier == new Rune ('\xFFFF')) { + return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created"); + } + if (TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk)) { + if (_hotKey.KeyCode != hk && hk != KeyCode.Unknown) { + HotKey = hk; } + } else { + HotKey = KeyCode.Null; + } + } - if (KeyBindings.ContainsKey (key)) { - KeyBindings [key] = command; - } else { - KeyBindings.Add (key, command); + #endregion HotKey Support + + #region Tab/Focus Handling + // This is null, and allocated on demand. + List _tabIndexes; + + /// + /// Gets a list of the subviews that are s. + /// + /// The tabIndexes. + public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty; + + int _tabIndex = -1; + int _oldTabIndex; + + /// + /// Indicates the index of the current from the list. See also: . + /// + public int TabIndex { + get { return _tabIndex; } + set { + if (!CanFocus) { + _tabIndex = -1; + return; + } else if (SuperView?._tabIndexes == null || SuperView?._tabIndexes.Count == 1) { + _tabIndex = 0; + return; + } else if (_tabIndex == value) { + return; + } + _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value; + _tabIndex = GetTabIndex (_tabIndex); + if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) { + SuperView._tabIndexes.Remove (this); + SuperView._tabIndexes.Insert (_tabIndex, this); + SetTabIndex (); } } + } - /// - /// Replaces a key combination already bound to . - /// - /// The key to be replaced. - /// The new key to be used. - protected void ReplaceKeyBinding (Key fromKey, Key toKey) - { - if (KeyBindings.ContainsKey (fromKey)) { - var value = KeyBindings [fromKey]; - KeyBindings.Remove (fromKey); - KeyBindings [toKey] = value; + int GetTabIndex (int idx) + { + var i = 0; + foreach (var v in SuperView._tabIndexes) { + if (v._tabIndex == -1 || v == this) { + continue; } + i++; } + return Math.Min (i, idx); + } - /// - /// Checks if the key binding already exists. - /// - /// The key to check. - /// If the key already exist, otherwise. - public bool ContainsKeyBinding (Key key) - { - return KeyBindings.ContainsKey (key); - } - - /// - /// Removes all bound keys from the View and resets the default bindings. - /// - public void ClearKeyBindings () - { - KeyBindings.Clear (); - } - - /// - /// Clears the existing keybinding (if any) for the given . - /// - /// - public void ClearKeyBinding (Key key) - { - KeyBindings.Remove (key); - } - - /// - /// Removes all key bindings that trigger the given command. Views can have multiple different - /// keys bound to the same command and this method will clear all of them. - /// - /// - public void ClearKeyBinding (params Command [] command) - { - foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) { - KeyBindings.Remove (kvp.Key); + void SetTabIndex () + { + var i = 0; + foreach (var v in SuperView._tabIndexes) { + if (v._tabIndex == -1) { + continue; } + v._tabIndex = i; + i++; } + } - /// - /// States that the given supports a given - /// and what to perform to make that command happen - /// - /// If the already has an implementation the - /// will replace the old one - /// - /// The command. - /// The function. - protected void AddCommand (Command command, Func f) - { - // if there is already an implementation of this command - if (CommandImplementations.ContainsKey (command)) { - // replace that implementation - CommandImplementations [command] = f; - } else { - // else record how to perform the action (this should be the normal case) - CommandImplementations.Add (command, f); + bool _tabStop = true; + + /// + /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be + /// only if the is also . + /// Set to to prevent the view from being a stop-point for keyboard navigation. + /// + /// + /// The default keyboard navigation keys are Key.Tab and Key>Tab.WithShift. + /// These can be changed by modifying the key bindings (see ) of the SuperView. + /// + public bool TabStop { + get => _tabStop; + set { + if (_tabStop == value) { + return; } + _tabStop = CanFocus && value; } + } - /// - /// Returns all commands that are supported by this . - /// - /// - public IEnumerable GetSupportedCommands () - { - return CommandImplementations.Keys; + #endregion Tab/Focus Handling + + #region Low-level Key handling + + #region Key Down Event + /// + /// If the view is enabled, processes a new key down event and returns if the event was handled. + /// + /// + /// + /// If the view has a sub view that is focused, will be called on the focused view first. + /// + /// + /// If the focused sub view does not handle the key press, this method calls to allow the view + /// to pre-process the key press. If returns , this method then calls + /// to invoke any key bindings. Then, only if no key bindings are handled, + /// will be called allowing the view to process the key press. + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// + /// if the event was handled. + public bool NewKeyDownEvent (Key keyEvent) + { + if (!Enabled) { + return false; } - /// - /// Gets the key used by a command. - /// - /// The command to search. - /// The used by a - public Key GetKeyFromCommand (params Command [] command) - { - return KeyBindings.First (kb => kb.Value.SequenceEqual (command)).Key; + // By default the KeyBindingScope is View + + if (Focused?.NewKeyDownEvent (keyEvent) == true) { + return true; } - /// - public override bool ProcessHotKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } + // Before (fire the cancellable event) + if (OnKeyDown (keyEvent)) { + return true; + } - var args = new KeyEventEventArgs (keyEvent); - if (MostFocused?.Enabled == true) { - MostFocused?.KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; - } - if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true) - return true; - if (_subviews == null || _subviews.Count == 0) - return false; + // During (this is what can be cancelled) + var handled = OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + return true; + } - foreach (var view in _subviews) - if (view.Enabled && view.ProcessHotKey (keyEvent)) - return true; - return false; + // TODO: The below is not right. OnXXX handlers are supposed to fire the events. + // TODO: But I've moved it outside of the v-function to test something. + // After (fire the cancellable event) + // fire event + ProcessKeyDown?.Invoke (this, keyEvent); + if (!keyEvent.Handled && OnProcessKeyDown (keyEvent)) { + return true; } - /// - public override bool ProcessColdKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } - var args = new KeyEventEventArgs (keyEvent); - KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; - if (MostFocused?.Enabled == true) { - MostFocused?.KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; - } - if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true) - return true; - if (_subviews == null || _subviews.Count == 0) - return false; + return keyEvent.Handled; + } - foreach (var view in _subviews) - if (view.Enabled && view.ProcessColdKey (keyEvent)) - return true; + /// + /// Low-level API called when the user presses a key, allowing a view to pre-process the key down event. + /// This is called from before . + /// + /// Contains the details about the key that produced the event. + /// if the key press was not handled. if + /// the keypress was handled and no other view should see it. + /// + /// + /// For processing s and commands, use and instead. + /// + /// + /// Fires the event. + /// + /// + public virtual bool OnKeyDown (Key keyEvent) + { + // fire event + KeyDown?.Invoke (this, keyEvent); + return keyEvent.Handled; + } + + /// + /// Invoked when the user presses a key, allowing subscribers to pre-process the key down event. + /// This is fired from before . + /// Set to true to stop the key from + /// being processed by other views. + /// + /// + /// + /// Not all terminals support key distinct up notifications, Applications should avoid + /// depending on distinct KeyUp events. + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + public event EventHandler KeyDown; + + /// + /// Low-level API called when the user presses a key, allowing views do things during key down events. + /// This is called from after . + /// + /// Contains the details about the key that produced the event. + /// if the key press was not handled. if + /// the keypress was handled and no other view should see it. + /// + /// + /// Override to override the behavior of how the base class processes key down events. + /// + /// + /// For processing s and commands, use and instead. + /// + /// + /// Fires the event. + /// + /// + /// Not all terminals support distinct key up notifications; applications should avoid + /// depending on distinct KeyUp events. + /// + /// + public virtual bool OnProcessKeyDown (Key keyEvent) + { + //ProcessKeyDown?.Invoke (this, keyEvent); + return keyEvent.Handled; + } + + /// + /// Invoked when the users presses a key, allowing subscribers to do things during key down events. + /// Set to true to stop the key from + /// being processed by other views. Invoked after and before . + /// + /// + /// + /// SubViews can use the of their super view override the default behavior of + /// when key bindings are invoked. + /// + /// + /// Not all terminals support distinct key up notifications; applications should avoid + /// depending on distinct KeyUp events. + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + public event EventHandler ProcessKeyDown; + + #endregion KeyDown Event + + #region KeyUp Event + /// + /// If the view is enabled, processes a new key up event and returns if the event was handled. + /// Called before . + /// + /// + /// + /// Not all terminals support key distinct down/up notifications, Applications should avoid + /// depending on distinct KeyUp events. + /// + /// + /// If the view has a sub view that is focused, will be called on the focused view first. + /// + /// + /// If the focused sub view does not handle the key press, this method calls , which is cancellable. + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// + /// if the event was handled. + public bool NewKeyUpEvent (Key keyEvent) + { + if (!Enabled) { return false; } - /// - /// Invoked when a key is pressed. - /// - public event EventHandler KeyDown; + if (Focused?.NewKeyUpEvent (keyEvent) == true) { + return true; + } - /// - public override bool OnKeyDown (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } + // Before (fire the cancellable event) + if (OnKeyUp (keyEvent)) { + return true; + } - var args = new KeyEventEventArgs (keyEvent); - KeyDown?.Invoke (this, args); - if (args.Handled) { - return true; - } - if (Focused?.Enabled == true) { - Focused.KeyDown?.Invoke (this, args); - if (args.Handled) { - return true; - } - if (Focused?.OnKeyDown (keyEvent) == true) { - return true; - } - } + // During (this is what can be cancelled) + // TODO: Until there's a clear use-case, we will not define 'during' event (e.g. OnDuringKeyUp). - return false; + // After (fire the cancellable event InvokingKeyBindings) + // TODO: Until there's a clear use-case, we will not define an 'after' event (e.g. OnAfterKeyUp). + + return false; + } + + /// + /// Method invoked when a key is released. This method is called from . + /// + /// Contains the details about the key that produced the event. + /// if the key stroke was not handled. if no + /// other view should see it. + /// + /// Not all terminals support key distinct down/up notifications, Applications should avoid + /// depending on distinct KeyUp events. + /// + /// Overrides must call into the base and return if the base returns . + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + public virtual bool OnKeyUp (Key keyEvent) + { + // fire event + KeyUp?.Invoke (this, keyEvent); + if (keyEvent.Handled) { + return true; } - /// - /// Invoked when a key is released. - /// - public event EventHandler KeyUp; + return false; + } - /// - public override bool OnKeyUp (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } + /// + /// Invoked when a key is released. Set to true to stop the key up event from being processed by other views. + /// + /// Not all terminals support key distinct down/up notifications, Applications should avoid + /// depending on distinct KeyDown and KeyUp events and instead should use . + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// + public event EventHandler KeyUp; + + #endregion KeyUp Event + + #endregion Low-level Key handling + + #region Key Bindings + + /// + /// Gets the key bindings for this view. + /// + public KeyBindings KeyBindings { get; } = new (); + private Dictionary> CommandImplementations { get; } = new (); + + /// + /// Low-level API called when a user presses a key; invokes any key bindings set on the view. + /// This is called during after has returned. + /// + /// + /// + /// Fires the event. + /// + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// Contains the details about the key that produced the event. + /// 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) + { + // fire event + // BUGBUG: KeyEventArgs doesn't include scope, so the event never sees it. + InvokingKeyBindings?.Invoke (this, keyEvent); + if (keyEvent.Handled) { + return true; + } - var args = new KeyEventEventArgs (keyEvent); - KeyUp?.Invoke (this, args); - if (args.Handled) { - return true; - } - if (Focused?.Enabled == true) { - Focused.KeyUp?.Invoke (this, args); - if (args.Handled) { - return true; - } - if (Focused?.OnKeyUp (keyEvent) == true) { + // * If no key binding was found, `InvokeKeyBindings` returns `null`. + // Continue passing the event (return `false` from `OnInvokeKeyBindings`). + // * If key bindings were found, but none handled the key (all `Command`s returned `false`), + // `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`). + var handled = InvokeKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + // Stop processing if any key binding handled the key. + // DO NOT stop processing if there are no matching key bindings or none of the key bindings handled the key + return true; + } + + // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey. + foreach (var view in Subviews.Where (v => v.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.HotKey, out var _))) { + // TODO: I think this TryGet is not needed due to the one in the lambda above. Use `Get` instead? + if (view.KeyBindings.TryGet (keyEvent.KeyCode, KeyBindingScope.HotKey, out var binding)) { + keyEvent.Scope = KeyBindingScope.HotKey; + handled = view.OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { return true; } } + } - return false; + return handled; + } + + /// + /// 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. + /// + public event EventHandler InvokingKeyBindings; + + /// + /// Invokes any binding that is registered on this + /// and matches the + /// + /// See for an overview of Terminal.Gui keyboard APIs. + /// + /// + /// The key event passed. + /// + /// if no command was bound the . + /// if commands were invoked and at least one handled the command. + /// if commands were invoked and at none handled the command. + /// + protected bool? InvokeKeyBindings (Key keyEvent) + { + bool? toReturn = null; + var key = keyEvent.KeyCode; + if (!KeyBindings.TryGet (key, out var binding)) { + return null; } - - void SetHotKey () - { - if (TextFormatter == null) { - return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created"); + foreach (var command in binding.Commands) { + + if (!CommandImplementations.ContainsKey (command)) { + throw new NotSupportedException (@$"A KeyBinding was set up for the command {command} ({keyEvent.KeyCode}) but that command is not supported by this View ({GetType ().Name})"); } - TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk); - if (_hotKey != hk) { - HotKey = hk; + + // each command has its own return value + var thisReturn = InvokeCommand (command); + + // if we haven't got anything yet, the current command result should be used + toReturn ??= thisReturn; + + // if ever see a true then that's what we will return + if (thisReturn ?? false) { + toReturn = true; } } + + return toReturn; + } + + /// + /// Invokes the specified command. + /// + /// + /// + /// 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. + /// + public bool? InvokeCommand (Command command) + { + if (!CommandImplementations.ContainsKey (command)) { + return null; + } + return CommandImplementations [command] (); } + + /// + /// + /// Sets the function that will be invoked for a . Views should call + /// for each command they support. + /// + /// + /// If has already been called for will replace the old one. + /// + /// The command. + /// The function. + protected void AddCommand (Command command, Func f) + { + // if there is already an implementation of this command + // replace that implementation + // else record how to perform the action (this should be the normal case) + if (CommandImplementations != null) { + CommandImplementations [command] = f; + } + } + + /// + /// Returns all commands that are supported by this . + /// + /// + public IEnumerable GetSupportedCommands () + { + return CommandImplementations.Keys; + } + + // TODO: Add GetKeysBoundToCommand() - given a Command, return all Keys that would invoke it + + #endregion Key Bindings } diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 8a5b2dd52a..649622bbb7 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -48,11 +48,6 @@ public virtual string Text { /// public TextFormatter TextFormatter { get; set; } - void TextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e) - { - HotKeyChanged?.Invoke (this, e); - } - /// /// Can be overridden if the has /// different format than the default. diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 5cd1605dd9..897ed3beb4 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -8,304 +8,229 @@ using System; using System.Text; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Button is a that provides an item that invokes raises the event. +/// +/// +/// +/// Provides a button showing text that raises the event when clicked on with a mouse +/// or when the user presses SPACE, ENTER, or the . The hot key is the first letter or digit following the first underscore ('_') +/// in the button text. +/// +/// +/// Use to change the hot key specifier from the default of ('_'). +/// +/// +/// If no hot key specifier is found, the first uppercase letter encountered will be used as the hot key. +/// +/// +/// When the button is configured as the default () and the user presses +/// the ENTER key, if no other processes the key, the 's +/// event will will be fired. +/// +/// +public class Button : View { + bool _isDefault; + Rune _leftBracket; + Rune _rightBracket; + Rune _leftDefault; + Rune _rightDefault; + /// - /// Button is a that provides an item that invokes raises the event. + /// Initializes a new instance of using layout. /// /// - /// - /// Provides a button showing text that raises the event when clicked on with a mouse - /// or when the user presses SPACE, ENTER, or hotkey. The hotkey is the first letter or digit following the first underscore ('_') - /// in the button text. - /// - /// - /// Use to change the hotkey specifier from the default of ('_'). - /// - /// - /// If no hotkey specifier is found, the first uppercase letter encountered will be used as the hotkey. - /// - /// - /// When the button is configured as the default () and the user presses - /// the ENTER key, if no other processes the , the 's - /// event will will be fired. - /// + /// The width of the is computed based on the + /// text length. The height will always be 1. /// - public class Button : View { - bool is_default; - Rune _leftBracket; - Rune _rightBracket; - Rune _leftDefault; - Rune _rightDefault; - - /// - /// Initializes a new instance of using layout. - /// - /// - /// The width of the is computed based on the - /// text length. The height will always be 1. - /// - public Button () : this (text: string.Empty, is_default: false) { } - - /// - /// Initializes a new instance of using layout. - /// - /// - /// The width of the is computed based on the - /// text length. The height will always be 1. - /// - /// The button's text - /// - /// If true, a special decoration is used, and the user pressing the enter key - /// in a will implicitly activate this button. - /// - public Button (string text, bool is_default = false) : base (text) - { - SetInitialProperties (text, is_default); - } - - /// - /// Initializes a new instance of using layout, based on the given text - /// - /// - /// The width of the is computed based on the - /// text length. The height will always be 1. - /// - /// X position where the button will be shown. - /// Y position where the button will be shown. - /// The button's text - public Button (int x, int y, string text) : this (x, y, text, false) { } + public Button () : this (text: string.Empty, is_default: false) { } - /// - /// Initializes a new instance of using layout, based on the given text. - /// - /// - /// The width of the is computed based on the - /// text length. The height will always be 1. - /// - /// X position where the button will be shown. - /// Y position where the button will be shown. - /// The button's text - /// - /// If true, a special decoration is used, and the user pressing the enter key - /// in a will implicitly activate this button. - /// - public Button (int x, int y, string text, bool is_default) - : base (new Rect (x, y, text.GetRuneCount () + 4 + (is_default ? 2 : 0), 1), text) - { - SetInitialProperties (text, is_default); - } - // TODO: v2 - Remove constructors with parameters - /// - /// Private helper to set the initial properties of the View that were provided via constructors. - /// - /// - /// - void SetInitialProperties (string text, bool is_default) - { - TextAlignment = TextAlignment.Centered; - VerticalTextAlignment = VerticalTextAlignment.Middle; + /// + /// Initializes a new instance of using layout. + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// The button's text + /// + /// If true, a special decoration is used, and the user pressing the enter key + /// in a will implicitly activate this button. + /// + public Button (string text, bool is_default = false) : base (text) + { + SetInitialProperties (text, is_default); + } - HotKeySpecifier = new Rune ('_'); + /// + /// Initializes a new instance of using layout, based on the given text + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// X position where the button will be shown. + /// Y position where the button will be shown. + /// The button's text + public Button (int x, int y, string text) : this (x, y, text, false) { } - _leftBracket = CM.Glyphs.LeftBracket; - _rightBracket = CM.Glyphs.RightBracket; - _leftDefault = CM.Glyphs.LeftDefaultIndicator; - _rightDefault = CM.Glyphs.RightDefaultIndicator; + /// + /// Initializes a new instance of using layout, based on the given text. + /// + /// + /// The width of the is computed based on the + /// text length. The height will always be 1. + /// + /// X position where the button will be shown. + /// Y position where the button will be shown. + /// The button's text + /// + /// If true, a special decoration is used, and the user pressing the enter key + /// in a will implicitly activate this button. + /// + public Button (int x, int y, string text, bool is_default) + : base (new Rect (x, y, text.GetRuneCount () + 4 + (is_default ? 2 : 0), 1), text) + { + SetInitialProperties (text, is_default); + } - CanFocus = true; - AutoSize = true; - this.is_default = is_default; - Text = text ?? string.Empty; + // TODO: v2 - Remove constructors with parameters + /// + /// Private helper to set the initial properties of the View that were provided via constructors. + /// + /// + /// + void SetInitialProperties (string text, bool is_default) + { + TextAlignment = TextAlignment.Centered; + VerticalTextAlignment = VerticalTextAlignment.Middle; + + HotKeySpecifier = new Rune ('_'); + + _leftBracket = CM.Glyphs.LeftBracket; + _rightBracket = CM.Glyphs.RightBracket; + _leftDefault = CM.Glyphs.LeftDefaultIndicator; + _rightDefault = CM.Glyphs.RightDefaultIndicator; + + CanFocus = true; + AutoSize = true; + _isDefault = is_default; + Text = text ?? string.Empty; + + OnResizeNeeded (); + + // Override default behavior of View + // Command.Default sets focus + AddCommand (Command.Accept, () => { OnClicked (); return true; }); + KeyBindings.Add (Key.Space, Command.Default, Command.Accept); + } + /// + /// Gets or sets whether the is the default action to activate in a dialog. + /// + /// true if is default; otherwise, false. + public bool IsDefault { + get => _isDefault; + set { + _isDefault = value; + UpdateTextFormatterText (); OnResizeNeeded (); - - // Things this view knows how to do - AddCommand (Command.Accept, () => AcceptKey ()); - - // Default keybindings for this view - AddKeyBinding (Key.Enter, Command.Accept); - AddKeyBinding (Key.Space, Command.Accept); - if (HotKey != Key.Null) { - AddKeyBinding (Key.Space | HotKey, Command.Accept); - } } + } - /// - /// Gets or sets whether the is the default action to activate in a dialog. - /// - /// true if is default; otherwise, false. - public bool IsDefault { - get => is_default; - set { - is_default = value; - UpdateTextFormatterText (); - OnResizeNeeded (); - } - } + /// + /// + /// + public bool NoDecorations { get; set; } - /// - public override Key HotKey { - get => base.HotKey; - set { - if (base.HotKey != value) { - var v = value == Key.Unknown ? Key.Null : value; - if (base.HotKey != Key.Null && ContainsKeyBinding (Key.Space | base.HotKey)) { - if (v == Key.Null) { - ClearKeyBinding (Key.Space | base.HotKey); - } else { - ReplaceKeyBinding (Key.Space | base.HotKey, Key.Space | v); - } - } else if (v != Key.Null) { - AddKeyBinding (Key.Space | v, Command.Accept); - } - base.HotKey = TextFormatter.HotKey = v; - } + /// + /// + /// + public bool NoPadding { get; set; } + + /// + protected override void UpdateTextFormatterText () + { + if (NoDecorations) { + TextFormatter.Text = Text; + } else + if (IsDefault) + TextFormatter.Text = $"{_leftBracket}{_leftDefault} {Text} {_rightDefault}{_rightBracket}"; + else { + if (NoPadding) { + TextFormatter.Text = $"{_leftBracket}{Text}{_rightBracket}"; + } else { + TextFormatter.Text = $"{_leftBracket} {Text} {_rightBracket}"; } } + } - /// - /// - /// - public bool NoDecorations { get; set; } + bool AcceptKey () + { + //if (!HasFocus) { + // SetFocus (); + //} + OnClicked (); + return true; + } - /// - /// - /// - public bool NoPadding { get; set; } + /// + /// Virtual method to invoke the event. + /// + public virtual void OnClicked () + { + Clicked?.Invoke (this, EventArgs.Empty); + } - /// - protected override void UpdateTextFormatterText () - { - if (NoDecorations) { - TextFormatter.Text = Text; - } else - if (IsDefault) - TextFormatter.Text = $"{_leftBracket}{_leftDefault} {Text} {_rightDefault}{_rightBracket}"; - else { - if (NoPadding) { - TextFormatter.Text = $"{_leftBracket}{Text}{_rightBracket}"; - } else { - TextFormatter.Text = $"{_leftBracket} {Text} {_rightBracket}"; + /// + /// The event fired when the user clicks the primary mouse button within the Bounds of this + /// or if the user presses the action key while this view is focused. (TODO: IsDefault) + /// + /// + /// Client code can hook up to this event, it is + /// raised when the button is activated either with + /// the mouse or the keyboard. + /// + public event EventHandler Clicked; + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags == MouseFlags.Button1Clicked) { + if (CanFocus && Enabled) { + if (!HasFocus) { + SetFocus (); + SetNeedsDisplay (); + Draw (); } + OnClicked (); } - } - - /// - public override bool ProcessHotKey (KeyEvent kb) - { - if (!Enabled) { - return false; - } - - return ExecuteHotKey (kb); - } - - /// - public override bool ProcessColdKey (KeyEvent kb) - { - if (!Enabled) { - return false; - } - - return ExecuteColdKey (kb); - } - - /// - public override bool ProcessKey (KeyEvent kb) - { - if (!Enabled) { - return false; - } - - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - return base.ProcessKey (kb); - } - - bool ExecuteHotKey (KeyEvent ke) - { - if (ke.Key == (Key.AltMask | HotKey)) { - return AcceptKey (); - } - return false; - } - - bool ExecuteColdKey (KeyEvent ke) - { - if (IsDefault && ke.KeyValue == '\n') { - return AcceptKey (); - } - return ExecuteHotKey (ke); - } - bool AcceptKey () - { - if (!HasFocus) { - SetFocus (); - } - OnClicked (); return true; } + return false; + } - /// - /// Virtual method to invoke the event. - /// - public virtual void OnClicked () - { - Clicked?.Invoke (this, EventArgs.Empty); - } - - /// - /// The event fired when the user clicks the primary mouse button within the Bounds of this - /// or if the user presses the action key while this view is focused. (TODO: IsDefault) - /// - /// - /// Client code can hook up to this event, it is - /// raised when the button is activated either with - /// the mouse or the keyboard. - /// - public event EventHandler Clicked; - - /// - public override bool MouseEvent (MouseEvent me) - { - if (me.Flags == MouseFlags.Button1Clicked) { - if (CanFocus && Enabled) { - if (!HasFocus) { - SetFocus (); - SetNeedsDisplay (); - Draw (); - } - OnClicked (); - } - - return true; - } - return false; - } - - /// - public override void PositionCursor () - { - if (HotKey == Key.Unknown && Text != "") { - for (int i = 0; i < TextFormatter.Text.GetRuneCount (); i++) { - if (TextFormatter.Text [i] == Text [0]) { - Move (i, 0); - return; - } + /// + public override void PositionCursor () + { + if (HotKey.IsValid && Text != "") { + for (int i = 0; i < TextFormatter.Text.GetRuneCount (); i++) { + if (TextFormatter.Text [i] == Text [0]) { + Move (i, 0); + return; } } - base.PositionCursor (); } + base.PositionCursor (); + } - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - return base.OnEnter (view); - } + return base.OnEnter (view); } } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index e8eaf8088f..81c48da605 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -1,235 +1,243 @@ -// -// Checkbox.cs: Checkbox control -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -using System; +using System; using System.Text; -namespace Terminal.Gui { +namespace Terminal.Gui; + +/// +/// The shows an on/off toggle that the user can set +/// +public class CheckBox : View { + Rune _charNullChecked; + Rune _charChecked; + Rune _charUnChecked; + bool? @_checked; + bool _allowNullChecked; /// - /// The shows an on/off toggle that the user can set + /// Toggled event, raised when the is toggled. /// - public class CheckBox : View { - Rune charNullChecked; - Rune charChecked; - Rune charUnChecked; - bool? @checked; - bool allowNullChecked; - - /// - /// Toggled event, raised when the is toggled. - /// - /// - /// Client code can hook up to this event, it is - /// raised when the is activated either with - /// the mouse or the keyboard. The passed bool contains the previous state. - /// - public event EventHandler Toggled; - - /// - /// Called when the property changes. Invokes the event. - /// - public virtual void OnToggled (ToggleEventArgs e) - { - Toggled?.Invoke (this, e); - } + /// + /// Client code can hook up to this event, it is + /// raised when the is activated either with + /// the mouse or the keyboard. The passed bool contains the previous state. + /// + public event EventHandler Toggled; - /// - /// Initializes a new instance of based on the given text, using layout. - /// - public CheckBox () : this (string.Empty) { } - - /// - /// Initializes a new instance of based on the given text, using layout. - /// - /// S. - /// If set to true is checked. - public CheckBox (string s, bool is_checked = false) : base () - { - SetInitialProperties (s, is_checked); - } - - /// - /// Initializes a new instance of using layout. - /// - /// - /// The size of is computed based on the - /// text length. This is not toggled. - /// - public CheckBox (int x, int y, string s) : this (x, y, s, false) - { - } - - /// - /// Initializes a new instance of using layout. - /// - /// - /// The size of is computed based on the - /// text length. - /// - public CheckBox (int x, int y, string s, bool is_checked) : base (new Rect (x, y, s.Length, 1)) - { - SetInitialProperties (s, is_checked); - } + /// + /// Called when the property changes. Invokes the event. + /// + public virtual void OnToggled (ToggleEventArgs e) + { + Toggled?.Invoke (this, e); + } - // TODO: v2 - Remove constructors with parameters - /// - /// Private helper to set the initial properties of the View that were provided via constructors. - /// - /// - /// - void SetInitialProperties (string s, bool is_checked) - { - charNullChecked = CM.Glyphs.NullChecked; - charChecked = CM.Glyphs.Checked; - charUnChecked = CM.Glyphs.UnChecked; - Checked = is_checked; - HotKeySpecifier = (Rune)'_'; - CanFocus = true; - AutoSize = true; - Text = s; + /// + /// Initializes a new instance of based on the given text, using layout. + /// + public CheckBox () : this (string.Empty) { } - OnResizeNeeded (); + /// + /// Initializes a new instance of based on the given text, using layout. + /// + /// S. + /// If set to true is checked. + public CheckBox (string s, bool is_checked = false) : base () + { + SetInitialProperties (s, is_checked); + } - // Things this view knows how to do - AddCommand (Command.ToggleChecked, () => ToggleChecked ()); + /// + /// Initializes a new instance of using layout. + /// + /// + /// The size of is computed based on the + /// text length. This is not toggled. + /// + public CheckBox (int x, int y, string s) : this (x, y, s, false) + { + } - // Default keybindings for this view - AddKeyBinding ((Key)' ', Command.ToggleChecked); - AddKeyBinding (Key.Space, Command.ToggleChecked); - } + /// + /// Initializes a new instance of using layout. + /// + /// + /// The size of is computed based on the + /// text length. + /// + public CheckBox (int x, int y, string s, bool is_checked) : base (new Rect (x, y, s.Length, 1)) + { + SetInitialProperties (s, is_checked); + } - /// - protected override void UpdateTextFormatterText () - { - switch (TextAlignment) { - case TextAlignment.Left: - case TextAlignment.Centered: - case TextAlignment.Justified: - TextFormatter.Text = $"{GetCheckedState ()} {GetFormatterText ()}"; - break; - case TextAlignment.Right: - TextFormatter.Text = $"{GetFormatterText ()} {GetCheckedState ()}"; - break; + // TODO: v2 - Remove constructors with parameters + /// + /// Private helper to set the initial properties of the View that were provided via constructors. + /// + /// + /// + void SetInitialProperties (string s, bool is_checked) + { + _charNullChecked = CM.Glyphs.NullChecked; + _charChecked = CM.Glyphs.Checked; + _charUnChecked = CM.Glyphs.UnChecked; + Checked = is_checked; + HotKeySpecifier = (Rune)'_'; + CanFocus = true; + AutoSize = true; + Text = s; + + OnResizeNeeded (); + + // Things this view knows how to do + AddCommand (Command.ToggleChecked, () => ToggleChecked ()); + AddCommand (Command.Accept, () => { + if (!HasFocus) { + SetFocus (); } - } + ToggleChecked (); + return true; + }); - Rune GetCheckedState () - { - return Checked switch { - true => charChecked, - false => charUnChecked, - var _ => charNullChecked - }; - } + // Default keybindings for this view + KeyBindings.Add (Key.Space, Command.ToggleChecked); + } - string GetFormatterText () - { - if (AutoSize || string.IsNullOrEmpty (Text) || Frame.Width <= 2) { - return Text; + + /// + public override Key HotKey { + get => base.HotKey; + set { + if (value is null || value.KeyCode is KeyCode.Unknown) { + throw new ArgumentException (nameof (value)); } - return Text [..Math.Min (Frame.Width - 2, Text.GetRuneCount ())]; - } - /// - /// The state of the - /// - public bool? Checked { - get => @checked; - set { - if (value == null && !AllowNullChecked) { - return; + var prev = base.HotKey; + if (prev != value) { + var v = value == KeyCode.Unknown ? Key.Empty: value; + base.HotKey = TextFormatter.HotKey = v; + + // Also add Alt+HotKey + if (prev != (Key)KeyCode.Null && KeyBindings.TryGet (prev.WithAlt, out _)) { + if (v.KeyCode == KeyCode.Null) { + KeyBindings.Remove (prev.WithAlt); + } else { + KeyBindings.Replace (prev.WithAlt, v.WithAlt); + } + } else if (v.KeyCode != KeyCode.Null) { + KeyBindings.Add (v.WithAlt, Command.Accept); } - @checked = value; - UpdateTextFormatterText (); - OnResizeNeeded (); } } + } - /// - /// If allows to be null, true or false. - /// If only allows to be true or false. - /// - public bool AllowNullChecked { - get => allowNullChecked; - set { - allowNullChecked = value; - Checked ??= false; - } + /// + protected override void UpdateTextFormatterText () + { + switch (TextAlignment) { + case TextAlignment.Left: + case TextAlignment.Centered: + case TextAlignment.Justified: + TextFormatter.Text = $"{GetCheckedState ()} {GetFormatterText ()}"; + break; + case TextAlignment.Right: + TextFormatter.Text = $"{GetFormatterText ()} {GetCheckedState ()}"; + break; } + } + + Rune GetCheckedState () + { + return Checked switch { + true => _charChecked, + false => _charUnChecked, + var _ => _charNullChecked + }; + } - /// - public override void PositionCursor () - { - Move (0, 0); + string GetFormatterText () + { + if (AutoSize || string.IsNullOrEmpty (Text) || Frame.Width <= 2) { + return Text; } + return Text [..Math.Min (Frame.Width - 2, Text.GetRuneCount ())]; + } - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; + /// + /// The state of the + /// + public bool? Checked { + get => @_checked; + set { + if (value == null && !AllowNullChecked) { + return; + } + @_checked = value; + UpdateTextFormatterText (); + OnResizeNeeded (); + } + } - return base.ProcessKey (kb); + /// + /// If allows to be null, true or false. + /// If only allows to be true or false. + /// + public bool AllowNullChecked { + get => _allowNullChecked; + set { + _allowNullChecked = value; + Checked ??= false; } + } - /// - public override bool ProcessHotKey (KeyEvent kb) - { - if (kb.Key == (Key.AltMask | HotKey)) - return ToggleChecked (); + /// + public override void PositionCursor () + { + Move (0, 0); + } - return false; + bool ToggleChecked () + { + if (!HasFocus) { + SetFocus (); } - - bool ToggleChecked () - { - if (!HasFocus) { - SetFocus (); - } - var previousChecked = Checked; - if (AllowNullChecked) { - switch (previousChecked) { - case null: - Checked = true; - break; - case true: - Checked = false; - break; - case false: - Checked = null; - break; - } - } else { - Checked = !Checked; + var previousChecked = Checked; + if (AllowNullChecked) { + switch (previousChecked) { + case null: + Checked = true; + break; + case true: + Checked = false; + break; + case false: + Checked = null; + break; } - - OnToggled (new ToggleEventArgs (previousChecked, Checked)); - SetNeedsDisplay (); - return true; + } else { + Checked = !Checked; } - /// - public override bool MouseEvent (MouseEvent me) - { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus) - return false; + OnToggled (new ToggleEventArgs (previousChecked, Checked)); + SetNeedsDisplay (); + return true; + } - ToggleChecked (); + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus) + return false; - return true; - } + ToggleChecked (); + + return true; + } - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - return base.OnEnter (view); - } + return base.OnEnter (view); } } diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 1cb98190cd..0c9b92da49 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -143,10 +143,10 @@ private void AddCommands () /// private void AddKeyBindings () { - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); } /// @@ -250,16 +250,6 @@ public virtual bool MoveDown () return true; } - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - return false; - } - /// public override bool MouseEvent (MouseEvent me) { diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 17d4e8ecfb..ddf24086db 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -343,16 +343,16 @@ private void Initialize () AddCommand (Command.UnixEmulation, () => UnixEmulation ()); // Default keybindings for this view - AddKeyBinding (Key.Enter, Command.Accept); - AddKeyBinding (Key.F4, Command.ToggleExpandCollapse); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.PageUp, Command.PageUp); - AddKeyBinding (Key.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); - AddKeyBinding (Key.Esc, Command.Cancel); - AddKeyBinding (Key.U | Key.CtrlMask, Command.UnixEmulation); + KeyBindings.Add (KeyCode.Enter, Command.Accept); + KeyBindings.Add (KeyCode.F4, Command.ToggleExpandCollapse); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.Esc, Command.Cancel); + KeyBindings.Add (KeyCode.U | KeyCode.CtrlMask, Command.UnixEmulation); } private bool isShow = false; @@ -544,16 +544,6 @@ public override void OnDrawContent (Rect contentArea) Driver.AddRune (CM.Glyphs.DownArrow); } - /// - public override bool ProcessKey (KeyEvent e) - { - var result = InvokeKeybindings (e); - if (result != null) - return (bool)result; - - return base.ProcessKey (e); - } - bool UnixEmulation () { // Unix emulation diff --git a/Terminal.Gui/Views/ContextMenu.cs b/Terminal.Gui/Views/ContextMenu.cs deleted file mode 100644 index 4f448cc878..0000000000 --- a/Terminal.Gui/Views/ContextMenu.cs +++ /dev/null @@ -1,234 +0,0 @@ -using System; - -namespace Terminal.Gui { - /// - /// ContextMenu provides a pop-up menu that can be positioned anywhere within a . - /// ContextMenu is analogous to and, once activated, works like a sub-menu - /// of a (but can be positioned anywhere). - /// - /// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of the ContextMenu frame - /// (either to the right or left, depending on where the ContextMenu is relative to the edge of the screen). By setting - /// to , this behavior can be changed such that all sub-menus are - /// drawn within the ContextMenu frame. - /// - /// - /// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to another key). - /// - /// - /// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling . - /// - /// - /// ContextMenus are located using screen using screen coordinates and appear above all other Views. - /// - /// - public sealed class ContextMenu : IDisposable { - private static MenuBar menuBar; - private Key key = Key.F10 | Key.ShiftMask; - private MouseFlags mouseFlags = MouseFlags.Button3Clicked; - private Toplevel container; - - /// - /// Initializes a context menu with no menu items. - /// - public ContextMenu () : this (0, 0, new MenuBarItem ()) { } - - /// - /// Initializes a context menu, with a specifying the parent/host of the menu. - /// - /// The host view. - /// The menu items for the context menu. - public ContextMenu (View host, MenuBarItem menuItems) : - this (host.Frame.X, host.Frame.Y, menuItems) - { - Host = host; - } - - /// - /// Initializes a context menu with menu items at a specific screen location. - /// - /// The left position (screen relative). - /// The top position (screen relative). - /// The menu items. - public ContextMenu (int x, int y, MenuBarItem menuItems) - { - if (IsShow) { - if (menuBar.SuperView != null) { - Hide (); - } - IsShow = false; - } - MenuItems = menuItems; - Position = new Point (x, y); - } - - private void MenuBar_MenuAllClosed (object sender, EventArgs e) - { - Dispose (); - } - - /// - /// Disposes the context menu object. - /// - public void Dispose () - { - if (IsShow) { - menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; - menuBar.Dispose (); - menuBar = null; - IsShow = false; - } - if (container != null) { - container.Closing -= Container_Closing; - } - } - - /// - /// Shows (opens) the ContextMenu, displaying the s it contains. - /// - public void Show () - { - if (menuBar != null) { - Hide (); - } - container = Application.Current; - container.Closing += Container_Closing; - var frame = new Rect (0, 0, View.Driver.Cols, View.Driver.Rows); - var position = Position; - if (Host != null) { - Host.BoundsToScreen (frame.X, frame.Y, out int x, out int y); - var pos = new Point (x, y); - pos.Y += Host.Frame.Height - 1; - if (position != pos) { - Position = position = pos; - } - } - var rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children); - if (rect.Right >= frame.Right) { - if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero) { - position.X = frame.Right - rect.Width; - } else if (ForceMinimumPosToZero) { - position.X = 0; - } - } else if (ForceMinimumPosToZero && position.X < 0) { - position.X = 0; - } - if (rect.Bottom >= frame.Bottom) { - if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero) { - if (Host == null) { - position.Y = frame.Bottom - rect.Height - 1; - } else { - Host.BoundsToScreen (frame.X, frame.Y, out int x, out int y); - var pos = new Point (x, y); - position.Y = pos.Y - rect.Height - 1; - } - } else if (ForceMinimumPosToZero) { - position.Y = 0; - } - } else if (ForceMinimumPosToZero && position.Y < 0) { - position.Y = 0; - } - - menuBar = new MenuBar (new [] { MenuItems }) { - X = position.X, - Y = position.Y, - Width = 0, - Height = 0, - UseSubMenusSingleFrame = UseSubMenusSingleFrame, - Key = Key - }; - - menuBar.isContextMenuLoading = true; - menuBar.MenuAllClosed += MenuBar_MenuAllClosed; - IsShow = true; - menuBar.OpenMenu (); - } - - private void Container_Closing (object sender, ToplevelClosingEventArgs obj) - { - Hide (); - } - - /// - /// Hides (closes) the ContextMenu. - /// - public void Hide () - { - menuBar?.CleanUp (); - Dispose (); - } - - /// - /// Event invoked when the is changed. - /// - public event EventHandler KeyChanged; - - /// - /// Event invoked when the is changed. - /// - public event EventHandler MouseFlagsChanged; - - /// - /// Gets or sets the menu position. - /// - public Point Position { get; set; } - - /// - /// Gets or sets the menu items for this context menu. - /// - public MenuBarItem MenuItems { get; set; } - - /// - /// specifies they keyboard key that will activate the context menu with the keyboard. - /// - public Key Key { - get => key; - set { - var oldKey = key; - key = value; - KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, key)); - } - } - - /// - /// specifies the mouse action used to activate the context menu by mouse. - /// - public MouseFlags MouseFlags { - get => mouseFlags; - set { - var oldFlags = mouseFlags; - mouseFlags = value; - MouseFlagsChanged?.Invoke (this, new MouseFlagsChangedEventArgs (oldFlags, value)); - } - } - - /// - /// Gets whether the ContextMenu is showing or not. - /// - public static bool IsShow { get; private set; } - - /// - /// The host which position will be used, - /// otherwise if it's null the container will be used. - /// - public View Host { get; set; } - - /// - /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position - /// is less than zero. The default is which means the context menu will be forced to the right. - /// If set to , the context menu will be clipped on the left if x is less than zero. - /// - public bool ForceMinimumPosToZero { get; set; } = true; - - /// - /// Gets the that is hosting this context menu. - /// - public MenuBar MenuBar { get => menuBar; } - - /// - /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the ContextMenu - /// and any sub-menus that would normally cascade will be displayed within a single frame. If (the default), - /// sub-menus will cascade using separate frames for each level of the menu hierarchy. - /// - public bool UseSubMenusSingleFrame { get; set; } - } -} diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 2da853331f..56ed3d8b1a 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -10,419 +10,424 @@ using System.Linq; using System.Text; -namespace Terminal.Gui { +namespace Terminal.Gui; + +/// +/// Simple Date editing +/// +/// +/// The provides date editing functionality with mouse support. +/// +public class DateField : TextField { + DateTime date; + bool isShort; + int longFieldLen = 10; + int shortFieldLen = 8; + string sepChar; + string longFormat; + string shortFormat; + + int fieldLen => isShort ? shortFieldLen : longFieldLen; + + string format => isShort ? shortFormat : longFormat; + /// - /// Simple Date editing + /// DateChanged event, raised when the property has changed. /// /// - /// The provides date editing functionality with mouse support. + /// This event is raised when the property changes. /// - public class DateField : TextField { - DateTime date; - bool isShort; - int longFieldLen = 10; - int shortFieldLen = 8; - string sepChar; - string longFormat; - string shortFormat; - - int fieldLen => isShort ? shortFieldLen : longFieldLen; - string format => isShort ? shortFormat : longFormat; - - /// - /// DateChanged event, raised when the property has changed. - /// - /// - /// This event is raised when the property changes. - /// - /// - /// The passed event arguments containing the old value, new value, and format string. - /// - public event EventHandler> DateChanged; - - /// - /// Initializes a new instance of using layout. - /// - /// The x coordinate. - /// The y coordinate. - /// Initial date contents. - /// If true, shows only two digits for the year. - public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") - { - Initialize (date, isShort); - } + /// + /// The passed event arguments containing the old value, new value, and format string. + /// + public event EventHandler> DateChanged; - /// - /// Initializes a new instance of using layout. - /// - public DateField () : this (DateTime.MinValue) { } - - /// - /// Initializes a new instance of using layout. - /// - /// - public DateField (DateTime date) : base ("") - { - Width = fieldLen + 2; - Initialize (date); - } + /// + /// Initializes a new instance of using layout. + /// + /// The x coordinate. + /// The y coordinate. + /// Initial date contents. + /// If true, shows only two digits for the year. + public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") => Initialize (date, isShort); + + /// + /// Initializes a new instance of using layout. + /// + public DateField () : this (DateTime.MinValue) { } + + /// + /// Initializes a new instance of using layout. + /// + /// + public DateField (DateTime date) : base ("") + { + Width = fieldLen + 2; + Initialize (date); + } + + void Initialize (DateTime date, bool isShort = false) + { + var cultureInfo = CultureInfo.CurrentCulture; + sepChar = cultureInfo.DateTimeFormat.DateSeparator; + longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); + shortFormat = GetShortFormat (longFormat); + this.isShort = isShort; + Date = date; + CursorPosition = 1; + TextChanged += DateField_Changed; + + // Things this view knows how to do + AddCommand (Command.DeleteCharRight, () => { + DeleteCharRight (); + return true; + }); + AddCommand (Command.DeleteCharLeft, () => { + DeleteCharLeft (false); + return true; + }); + AddCommand (Command.LeftHome, () => MoveHome ()); + AddCommand (Command.Left, () => MoveLeft ()); + AddCommand (Command.RightEnd, () => MoveEnd ()); + AddCommand (Command.Right, () => MoveRight ()); + + // Default keybindings for this view + KeyBindings.Add (KeyCode.DeleteChar, Command.DeleteCharRight); + KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); + + KeyBindings.Add (Key.Delete, Command.DeleteCharLeft); + KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft); + + KeyBindings.Add (Key.Home, Command.LeftHome); + KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome); - void Initialize (DateTime date, bool isShort = false) - { - CultureInfo cultureInfo = CultureInfo.CurrentCulture; - sepChar = cultureInfo.DateTimeFormat.DateSeparator; - longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); - shortFormat = GetShortFormat (longFormat); - this.isShort = isShort; - Date = date; - CursorPosition = 1; - TextChanged += DateField_Changed; - - // Things this view knows how to do - AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); - AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; }); - AddCommand (Command.LeftHome, () => MoveHome ()); - AddCommand (Command.Left, () => MoveLeft ()); - AddCommand (Command.RightEnd, () => MoveEnd ()); - AddCommand (Command.Right, () => MoveRight ()); - - // Default keybindings for this view - AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); - AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); - - AddKeyBinding (Key.Delete, Command.DeleteCharLeft); - AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); - - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome); - - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); - - AddKeyBinding (Key.End, Command.RightEnd); - AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd); - - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); + KeyBindings.Add (Key.CursorLeft, Command.Left); + KeyBindings.Add (Key.B.WithCtrl, Command.Left); + + KeyBindings.Add (Key.End, Command.RightEnd); + KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd); + + KeyBindings.Add (Key.CursorRight, Command.Right); + KeyBindings.Add (Key.F.WithCtrl, Command.Right); + + } + + /// + public override bool OnProcessKeyDown (Key a) + { + // Ignore non-numeric characters. + if (a >= Key.D0 && a <= Key.D9) { + if (!ReadOnly) { + if (SetText ((Rune)a)) { + IncCursorPosition (); + } + } + return true; } + return false; + } - void DateField_Changed (object sender, TextChangedEventArgs e) - { - try { - if (!DateTime.TryParseExact (GetDate (Text), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) - Text = e.OldValue; - } catch (Exception) { + void DateField_Changed (object sender, TextChangedEventArgs e) + { + try { + if (!DateTime.TryParseExact (GetDate (Text), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) { Text = e.OldValue; } + } catch (Exception) { + Text = e.OldValue; } + } - string GetInvarianteFormat () - { - return $"MM{sepChar}dd{sepChar}yyyy"; - } + string GetInvarianteFormat () => $"MM{sepChar}dd{sepChar}yyyy"; - string GetLongFormat (string lf) - { - string [] frm = lf.Split (sepChar); - for (int i = 0; i < frm.Length; i++) { - if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2) - lf = lf.Replace ("M", "MM"); - if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2) - lf = lf.Replace ("d", "dd"); - if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4) - lf = lf.Replace ("yy", "yyyy"); + string GetLongFormat (string lf) + { + string [] frm = lf.Split (sepChar); + for (int i = 0; i < frm.Length; i++) { + if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2) { + lf = lf.Replace ("M", "MM"); + } + if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2) { + lf = lf.Replace ("d", "dd"); + } + if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4) { + lf = lf.Replace ("yy", "yyyy"); } - return $" {lf}"; } + return $" {lf}"; + } - string GetShortFormat (string lf) - { - return lf.Replace ("yyyy", "yy"); - } + string GetShortFormat (string lf) => lf.Replace ("yyyy", "yy"); - /// - /// Gets or sets the date of the . - /// - /// - /// - public DateTime Date { - get { - return date; + /// + /// Gets or sets the date of the . + /// + /// + /// + public DateTime Date { + get => date; + set { + if (ReadOnly) { + return; } - set { - if (ReadOnly) - return; - - var oldData = date; - date = value; - this.Text = value.ToString (format); - var args = new DateTimeEventArgs (oldData, value, format); - if (oldData != value) { - OnDateChanged (args); - } + + var oldData = date; + date = value; + Text = value.ToString (format); + var args = new DateTimeEventArgs (oldData, value, format); + if (oldData != value) { + OnDateChanged (args); } } + } - /// - /// Get or set the date format for the widget. - /// - public bool IsShortFormat { - get => isShort; - set { - isShort = value; - if (isShort) - Width = 10; - else - Width = 12; - var ro = ReadOnly; - if (ro) - ReadOnly = false; - SetText (Text); - ReadOnly = ro; - SetNeedsDisplay (); + /// + /// Get or set the date format for the widget. + /// + public bool IsShortFormat { + get => isShort; + set { + isShort = value; + if (isShort) { + Width = 10; + } else { + Width = 12; } + bool ro = ReadOnly; + if (ro) { + ReadOnly = false; + } + SetText (Text); + ReadOnly = ro; + SetNeedsDisplay (); } + } - /// - public override int CursorPosition { - get => base.CursorPosition; - set { - base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); - } + /// + public override int CursorPosition { + get => base.CursorPosition; + set => base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); + } + + bool SetText (Rune key) + { + var text = Text.EnumerateRunes ().ToList (); + var newText = text.GetRange (0, CursorPosition); + newText.Add (key); + if (CursorPosition < fieldLen) { + newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); } + return SetText (StringExtensions.ToString (newText)); + } - bool SetText (Rune key) - { - var text = Text.EnumerateRunes ().ToList (); - var newText = text.GetRange (0, CursorPosition); - newText.Add (key); - if (CursorPosition < fieldLen) - newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); - return SetText (StringExtensions.ToString (newText)); + bool SetText (string text) + { + if (string.IsNullOrEmpty (text)) { + return false; } - bool SetText (string text) - { - if (string.IsNullOrEmpty (text)) { - return false; - } + string [] vals = text.Split (sepChar); + string [] frm = format.Split (sepChar); + bool isValidDate = true; + int idx = GetFormatIndex (frm, "y"); + int year = Int32.Parse (vals [idx]); + int month; + int day; + idx = GetFormatIndex (frm, "M"); + if (Int32.Parse (vals [idx]) < 1) { + isValidDate = false; + month = 1; + vals [idx] = "1"; + } else if (Int32.Parse (vals [idx]) > 12) { + isValidDate = false; + month = 12; + vals [idx] = "12"; + } else { + month = Int32.Parse (vals [idx]); + } + idx = GetFormatIndex (frm, "d"); + if (Int32.Parse (vals [idx]) < 1) { + isValidDate = false; + day = 1; + vals [idx] = "1"; + } else if (Int32.Parse (vals [idx]) > 31) { + isValidDate = false; + day = DateTime.DaysInMonth (year, month); + vals [idx] = day.ToString (); + } else { + day = Int32.Parse (vals [idx]); + } + string d = GetDate (month, day, year, frm); - string [] vals = text.Split (sepChar); - string [] frm = format.Split (sepChar); - bool isValidDate = true; - int idx = GetFormatIndex (frm, "y"); - int year = Int32.Parse (vals [idx]); - int month; - int day; - idx = GetFormatIndex (frm, "M"); - if (Int32.Parse (vals [idx]) < 1) { - isValidDate = false; - month = 1; - vals [idx] = "1"; - } else if (Int32.Parse (vals [idx]) > 12) { - isValidDate = false; - month = 12; - vals [idx] = "12"; - } else - month = Int32.Parse (vals [idx]); - idx = GetFormatIndex (frm, "d"); - if (Int32.Parse (vals [idx]) < 1) { - isValidDate = false; - day = 1; - vals [idx] = "1"; - } else if (Int32.Parse (vals [idx]) > 31) { - isValidDate = false; - day = DateTime.DaysInMonth (year, month); - vals [idx] = day.ToString (); - } else - day = Int32.Parse (vals [idx]); - string d = GetDate (month, day, year, frm); - - if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || - !isValidDate) - return false; - Date = result; - return true; + if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result) || + !isValidDate) { + return false; } + Date = result; + return true; + } - string GetDate (int month, int day, int year, string [] fm) - { - string date = " "; - for (int i = 0; i < fm.Length; i++) { - if (fm [i].Contains ("M")) { - date += $"{month,2:00}"; - } else if (fm [i].Contains ("d")) { - date += $"{day,2:00}"; + string GetDate (int month, int day, int year, string [] fm) + { + string date = " "; + for (int i = 0; i < fm.Length; i++) { + if (fm [i].Contains ("M")) { + date += $"{month,2:00}"; + } else if (fm [i].Contains ("d")) { + date += $"{day,2:00}"; + } else { + if (!isShort && year.ToString ().Length == 2) { + string y = DateTime.Now.Year.ToString (); + date += y.Substring (0, 2) + year.ToString (); + } else if (isShort && year.ToString ().Length == 4) { + date += $"{year.ToString ().Substring (2, 2)}"; } else { - if (!isShort && year.ToString ().Length == 2) { - var y = DateTime.Now.Year.ToString (); - date += y.Substring (0, 2) + year.ToString (); - } else if (isShort && year.ToString ().Length == 4) { - date += $"{year.ToString ().Substring (2, 2)}"; - } else { - date += $"{year,2:00}"; - } + date += $"{year,2:00}"; } - if (i < 2) - date += $"{sepChar}"; } - return date; + if (i < 2) { + date += $"{sepChar}"; + } } + return date; + } - string GetDate (string text) - { - string [] vals = text.Split (sepChar); - string [] frm = format.Split (sepChar); - string [] date = { null, null, null }; - - for (int i = 0; i < frm.Length; i++) { - if (frm [i].Contains ("M")) { - date [0] = vals [i].Trim (); - } else if (frm [i].Contains ("d")) { - date [1] = vals [i].Trim (); + string GetDate (string text) + { + string [] vals = text.Split (sepChar); + string [] frm = format.Split (sepChar); + string [] date = { null, null, null }; + + for (int i = 0; i < frm.Length; i++) { + if (frm [i].Contains ("M")) { + date [0] = vals [i].Trim (); + } else if (frm [i].Contains ("d")) { + date [1] = vals [i].Trim (); + } else { + string year = vals [i].Trim (); + if (year.GetRuneCount () == 2) { + string y = DateTime.Now.Year.ToString (); + date [2] = y.Substring (0, 2) + year.ToString (); } else { - var year = vals [i].Trim (); - if (year.GetRuneCount () == 2) { - var y = DateTime.Now.Year.ToString (); - date [2] = y.Substring (0, 2) + year.ToString (); - } else { - date [2] = vals [i].Trim (); - } + date [2] = vals [i].Trim (); } } - return date [0] + sepChar + date [1] + sepChar + date [2]; } + return date [0] + sepChar + date [1] + sepChar + date [2]; + } - int GetFormatIndex (string [] fm, string t) - { - int idx = -1; - for (int i = 0; i < fm.Length; i++) { - if (fm [i].Contains (t)) { - idx = i; - break; - } + int GetFormatIndex (string [] fm, string t) + { + int idx = -1; + for (int i = 0; i < fm.Length; i++) { + if (fm [i].Contains (t)) { + idx = i; + break; } - return idx; } + return idx; + } - void IncCursorPosition () - { - if (CursorPosition == fieldLen) - return; - if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; + void IncCursorPosition () + { + if (CursorPosition == fieldLen) { + return; } - - void DecCursorPosition () - { - if (CursorPosition == 1) - return; - if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition--; + if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition++; } + } - void AdjCursorPosition () - { - if (Text [CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; + void DecCursorPosition () + { + if (CursorPosition == 1) { + return; } - - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) { - return (bool)result; - } - // Ignore non-numeric characters. - if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) { - return false; - } - - if (ReadOnly) { - return true; - } - - // BUGBUG: This is a hack, we should be able to just use ((Rune)(uint)kb.Key) directly. - if (SetText (((Rune)(uint)kb.Key).ToString ().EnumerateRunes ().First ())) { - IncCursorPosition (); - } - - return true; + if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition--; } + } - bool MoveRight () - { - IncCursorPosition (); - return true; + void AdjCursorPosition () + { + if (Text [CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition++; } + } - new bool MoveEnd () - { - CursorPosition = fieldLen; - return true; - } + bool MoveRight () + { + IncCursorPosition (); + return true; + } - bool MoveLeft () - { - DecCursorPosition (); - return true; - } + new bool MoveEnd () + { + CursorPosition = fieldLen; + return true; + } - bool MoveHome () - { - // Home, C-A - CursorPosition = 1; - return true; - } + bool MoveLeft () + { + DecCursorPosition (); + return true; + } - /// - public override void DeleteCharLeft (bool useOldCursorPos = true) - { - if (ReadOnly) { - return; - } + bool MoveHome () + { + // Home, C-A + CursorPosition = 1; + return true; + } - SetText ((Rune)'0'); - DecCursorPosition (); + /// + public override void DeleteCharLeft (bool useOldCursorPos = true) + { + if (ReadOnly) { return; } - /// - public override void DeleteCharRight () - { - if (ReadOnly) - return; + SetText ((Rune)'0'); + DecCursorPosition (); + return; + } - SetText ((Rune)'0'); + /// + public override void DeleteCharRight () + { + if (ReadOnly) { return; } - /// - public override bool MouseEvent (MouseEvent ev) - { - if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) - return false; - if (!HasFocus) - SetFocus (); - - var point = ev.X; - if (point > fieldLen) - point = fieldLen; - if (point < 1) - point = 1; - CursorPosition = point; - AdjCursorPosition (); - return true; + SetText ((Rune)'0'); + return; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) { + return false; + } + if (!HasFocus) { + SetFocus (); } - /// - /// Event firing method for the event. - /// - /// Event arguments - public virtual void OnDateChanged (DateTimeEventArgs args) - { - DateChanged?.Invoke (this, args); + int point = ev.X; + if (point > fieldLen) { + point = fieldLen; + } + if (point < 1) { + point = 1; } + CursorPosition = point; + AdjCursorPosition (); + return true; } + + /// + /// Event firing method for the event. + /// + /// Event arguments + public virtual void OnDateChanged (DateTimeEventArgs args) => DateChanged?.Invoke (this, args); } \ No newline at end of file diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index db7016394b..5e6165c080 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -228,15 +228,16 @@ void LayoutButtons () } } + // BUGBUG: Why is this not handled by a key binding??? /// - public override bool ProcessKey (KeyEvent kb) + public override bool OnProcessKeyDown (Key a) { - switch (kb.Key) { - case Key.Esc: + switch (a.KeyCode) { + case KeyCode.Esc: Application.RequestStop (this); return true; } - return base.ProcessKey (kb); + return false; } } } diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 95d637a662..9d64cae11c 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -142,22 +142,23 @@ public FileDialog (IFileSystem fileSystem) this.btnOk = new Button (Style.OkButtonText) { Y = Pos.AnchorEnd (1), - X = Pos.Function (CalculateOkButtonPosX) + X = Pos.Function (CalculateOkButtonPosX), + IsDefault = true }; this.btnOk.Clicked += (s, e) => this.Accept (true); - this.btnOk.KeyPressed += (s, k) => { - this.NavigateIf (k, Key.CursorLeft, this.btnCancel); - this.NavigateIf (k, Key.CursorUp, this.tableView); + this.btnOk.KeyDown += (s, k) => { + this.NavigateIf (k, KeyCode.CursorLeft, this.btnCancel); + this.NavigateIf (k, KeyCode.CursorUp, this.tableView); }; this.btnCancel = new Button (Strings.btnCancel) { Y = Pos.AnchorEnd (1), X = Pos.Right (btnOk) + 1 }; - this.btnCancel.KeyPressed += (s, k) => { - this.NavigateIf (k, Key.CursorLeft, this.btnToggleSplitterCollapse); - this.NavigateIf (k, Key.CursorUp, this.tableView); - this.NavigateIf (k, Key.CursorRight, this.btnOk); + this.btnCancel.KeyDown += (s, k) => { + this.NavigateIf (k, KeyCode.CursorLeft, this.btnToggleSplitterCollapse); + this.NavigateIf (k, KeyCode.CursorUp, this.tableView); + this.NavigateIf (k, KeyCode.CursorRight, this.btnOk); }; this.btnCancel.Clicked += (s, e) => { Application.RequestStop (); @@ -179,11 +180,11 @@ public FileDialog (IFileSystem fileSystem) Width = Dim.Fill (0), CaptionColor = new Color (Color.Black) }; - this.tbPath.KeyPressed += (s, k) => { + this.tbPath.KeyDown += (s, k) => { ClearFeedback (); - this.AcceptIf (k, Key.Enter); + this.AcceptIf (k, KeyCode.Enter); this.SuppressIfBadChar (k); }; @@ -207,7 +208,7 @@ public FileDialog (IFileSystem fileSystem) FullRowSelect = true, CollectionNavigator = new FileDialogCollectionNavigator (this) }; - this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); + this.tableView.KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); this.tableView.MouseClick += OnTableViewMouseClick; tableView.Style.InvertSelectedCellFirstCharacter = true; Style.TableStyle = tableView.Style; @@ -228,16 +229,16 @@ public FileDialog (IFileSystem fileSystem) typeStyle.MinWidth = 6; typeStyle.ColorGetter = this.ColorGetter; - this.tableView.KeyPressed += (s, k) => { + this.tableView.KeyDown += (s, k) => { if (this.tableView.SelectedRow <= 0) { - this.NavigateIf (k, Key.CursorUp, this.tbPath); + this.NavigateIf (k, KeyCode.CursorUp, this.tbPath); } if (this.tableView.SelectedRow == this.tableView.Table.Rows - 1) { - this.NavigateIf (k, Key.CursorDown, this.btnToggleSplitterCollapse); + this.NavigateIf (k, KeyCode.CursorDown, this.btnToggleSplitterCollapse); } if (splitContainer.Tiles.First ().ContentView.Visible && tableView.SelectedColumn == 0) { - this.NavigateIf (k, Key.CursorLeft, this.treeView); + this.NavigateIf (k, KeyCode.CursorLeft, this.treeView); } if (k.Handled) { @@ -277,6 +278,7 @@ public FileDialog (IFileSystem fileSystem) CaptionColor = new Color (Color.Black), Width = 30, Y = Pos.AnchorEnd (1), + HotKey = KeyCode.F | KeyCode.AltMask }; spinnerView = new SpinnerView () { X = Pos.Right (tbFind) + 1, @@ -285,22 +287,22 @@ public FileDialog (IFileSystem fileSystem) }; tbFind.TextChanged += (s, o) => RestartSearch (); - tbFind.KeyPressed += (s, o) => { - if (o.KeyEvent.Key == Key.Enter) { + tbFind.KeyDown += (s, o) => { + if (o.KeyCode == KeyCode.Enter) { RestartSearch (); o.Handled = true; } - if (o.KeyEvent.Key == Key.Esc) { + if (o.KeyCode == KeyCode.Esc) { if (CancelSearch ()) { o.Handled = true; } } if (tbFind.CursorIsAtEnd ()) { - NavigateIf (o, Key.CursorRight, btnCancel); + NavigateIf (o, KeyCode.CursorRight, btnCancel); } if (tbFind.CursorIsAtStart ()) { - NavigateIf (o, Key.CursorLeft, btnToggleSplitterCollapse); + NavigateIf (o, KeyCode.CursorLeft, btnToggleSplitterCollapse); } }; @@ -316,23 +318,23 @@ public FileDialog (IFileSystem fileSystem) this.tbPath.TextChanged += (s, e) => this.PathChanged (); this.tableView.CellActivated += this.CellActivate; - this.tableView.KeyUp += (s, k) => k.Handled = this.TableView_KeyUp (k.KeyEvent); + this.tableView.KeyUp += (s, k) => k.Handled = this.TableView_KeyUp (k); this.tableView.SelectedCellChanged += this.TableView_SelectedCellChanged; - this.tableView.AddKeyBinding (Key.Home, Command.TopHome); - this.tableView.AddKeyBinding (Key.End, Command.BottomEnd); - this.tableView.AddKeyBinding (Key.Home | Key.ShiftMask, Command.TopHomeExtend); - this.tableView.AddKeyBinding (Key.End | Key.ShiftMask, Command.BottomEndExtend); + this.tableView.KeyBindings.Add (KeyCode.Home, Command.TopHome); + this.tableView.KeyBindings.Add (KeyCode.End, Command.BottomEnd); + this.tableView.KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask, Command.TopHomeExtend); + this.tableView.KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask, Command.BottomEndExtend); this.treeView.KeyDown += (s, k) => { var selected = treeView.SelectedObject; if (selected != null) { if (!treeView.CanExpand (selected) || treeView.IsExpanded (selected)) { - this.NavigateIf (k, Key.CursorRight, this.tableView); + this.NavigateIf (k, KeyCode.CursorRight, this.tableView); } else if (treeView.GetObjectRow (selected) == 0) { - this.NavigateIf (k, Key.CursorUp, this.tbPath); + this.NavigateIf (k, KeyCode.CursorUp, this.tbPath); } } @@ -340,7 +342,7 @@ public FileDialog (IFileSystem fileSystem) return; } - k.Handled = this.TreeView_KeyDown (k.KeyEvent); + k.Handled = this.TreeView_KeyDown (k); }; @@ -484,23 +486,26 @@ private IFileSystemInfo [] GetFocusedFiles () } - /// - public override bool ProcessHotKey (KeyEvent keyEvent) - { - if (this.NavigateIf (keyEvent, Key.CtrlMask | Key.F, this.tbFind)) { - return true; - } +// /// +// public override bool OnHotKey (KeyEventArgs keyEvent) +// { +//#if BROKE_IN_2927 +// // BUGBUG: Ctrl-F is forward in a TextField. +// if (this.NavigateIf (keyEvent, Key.Alt | Key.F, this.tbFind)) { +// return true; +// } +//#endif - ClearFeedback (); +// ClearFeedback (); - if (allowedTypeMenuBar != null && - keyEvent.Key == Key.Tab && - allowedTypeMenuBar.IsMenuOpen) { - allowedTypeMenuBar.CloseMenu (false, false, false); - } +// if (allowedTypeMenuBar != null && +// keyEvent.ConsoleDriverKey == Key.Tab && +// allowedTypeMenuBar.IsMenuOpen) { +// allowedTypeMenuBar.CloseMenu (false, false, false); +// } - return base.ProcessHotKey (keyEvent); - } +// return base.OnHotKey (keyEvent); +// } private void RestartSearch () { if (disposed || State?.Directory == null) { @@ -772,19 +777,19 @@ private void AllowedTypeMenuClicked (int idx) } } - private void SuppressIfBadChar (KeyEventEventArgs k) + private void SuppressIfBadChar (Key k) { // don't let user type bad letters - var ch = (char)k.KeyEvent.KeyValue; + var ch = (char)k; if (badChars.Contains (ch)) { k.Handled = true; } } - private bool TreeView_KeyDown (KeyEvent keyEvent) + private bool TreeView_KeyDown (Key keyEvent) { - if (this.treeView.HasFocus && Separators.Contains ((char)keyEvent.KeyValue)) { + if (this.treeView.HasFocus && Separators.Contains ((char)keyEvent)) { this.tbPath.FocusFirst (); // let that keystroke go through on the tbPath instead @@ -794,9 +799,9 @@ private bool TreeView_KeyDown (KeyEvent keyEvent) return false; } - private void AcceptIf (KeyEventEventArgs keyEvent, Key isKey) + private void AcceptIf (Key keyEvent, KeyCode isKey) { - if (!keyEvent.Handled && keyEvent.KeyEvent.Key == isKey) { + if (!keyEvent.Handled && keyEvent.KeyCode == isKey) { keyEvent.Handled = true; // User hit Enter in text box so probably wants the @@ -880,19 +885,9 @@ private void FinishAccept () Application.RequestStop (); } - private void NavigateIf (KeyEventEventArgs keyEvent, Key isKey, View to) - { - if (!keyEvent.Handled) { - - if (NavigateIf (keyEvent.KeyEvent, isKey, to)) { - keyEvent.Handled = true; - } - } - } - - private bool NavigateIf (KeyEvent keyEvent, Key isKey, View to) + private bool NavigateIf (Key keyEvent, KeyCode isKey, View to) { - if (keyEvent.Key == isKey) { + if (keyEvent.KeyCode == isKey) { to.FocusFirst (); if (to == tbPath) { @@ -956,28 +951,28 @@ private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEv } } - private bool TableView_KeyUp (KeyEvent keyEvent) + private bool TableView_KeyUp (Key keyEvent) { - if (keyEvent.Key == Key.Backspace) { + if (keyEvent.KeyCode == KeyCode.Backspace) { return this.history.Back (); } - if (keyEvent.Key == (Key.ShiftMask | Key.Backspace)) { + if (keyEvent.KeyCode == (KeyCode.ShiftMask | KeyCode.Backspace)) { return this.history.Forward (); } - if (keyEvent.Key == Key.DeleteChar) { + if (keyEvent.KeyCode == KeyCode.DeleteChar) { Delete (); return true; } - if (keyEvent.Key == (Key.CtrlMask | Key.R)) { + if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.R)) { Rename (); return true; } - if (keyEvent.Key == (Key.CtrlMask | Key.N)) { + if (keyEvent.KeyCode == (KeyCode.CtrlMask | KeyCode.N)) { New (); return true; } diff --git a/Terminal.Gui/Views/GraphView/Annotations.cs b/Terminal.Gui/Views/GraphView/Annotations.cs index 1ef30c9f68..a2bbfc9cd6 100644 --- a/Terminal.Gui/Views/GraphView/Annotations.cs +++ b/Terminal.Gui/Views/GraphView/Annotations.cs @@ -17,7 +17,7 @@ namespace Terminal.Gui { public interface IAnnotation { /// /// True if annotation should be drawn before . This - /// allowes Series and later annotations to potentially draw over the top + /// allows Series and later annotations to potentially draw over the top /// of this annotation. /// bool BeforeSeries { get; } diff --git a/Terminal.Gui/Views/GraphView/GraphView.cs b/Terminal.Gui/Views/GraphView/GraphView.cs index 2d2310b1b2..32f579b389 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -81,14 +81,14 @@ public GraphView () AddCommand (Command.PageUp, () => { PageUp (); return true; }); AddCommand (Command.PageDown, () => { PageDown (); return true; }); - AddKeyBinding (Key.CursorRight, Command.ScrollRight); - AddKeyBinding (Key.CursorLeft, Command.ScrollLeft); - AddKeyBinding (Key.CursorUp, Command.ScrollUp); - AddKeyBinding (Key.CursorDown, Command.ScrollDown); + KeyBindings.Add (KeyCode.CursorRight, Command.ScrollRight); + KeyBindings.Add (KeyCode.CursorLeft, Command.ScrollLeft); + KeyBindings.Add (KeyCode.CursorUp, Command.ScrollUp); + KeyBindings.Add (KeyCode.CursorDown, Command.ScrollDown); // Not bound by default (preserves backwards compatibility) - //AddKeyBinding (Key.PageUp, Command.PageUp); - //AddKeyBinding (Key.PageDown, Command.PageDown); + //KeyBindings.Add (Key.PageUp, Command.PageUp); + //KeyBindings.Add (Key.PageDown, Command.PageDown); } /// @@ -243,18 +243,6 @@ public override bool OnEnter (View view) return base.OnEnter (view); } - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (HasFocus && CanFocus) { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; - } - - return base.ProcessKey (keyEvent); - } - /// /// Scrolls the graph up 1 page /// diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index b425e5f073..4e38d6a4b3 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -10,627 +10,655 @@ using System.IO; using System.Text; -namespace Terminal.Gui { +namespace Terminal.Gui; + +/// +/// An hex viewer and editor over a +/// +/// +/// +/// provides a hex editor on top of a seekable with the left side showing an hex +/// dump of the values in the and the right side showing the contents (filtered to +/// non-control sequence ASCII characters). +/// +/// +/// Users can switch from one side to the other by using the tab key. +/// +/// +/// To enable editing, set to true. When is true +/// the user can make changes to the hexadecimal values of the . Any changes are tracked +/// in the property (a ) indicating +/// the position where the changes were made and the new values. A convenience method, +/// will apply the edits to the . +/// +/// +/// Control the first byte shown by setting the property +/// to an offset in the stream. +/// +/// +public partial class HexView : View { + SortedDictionary edits = new SortedDictionary (); + Stream source; + long displayStart, pos; + bool firstNibble, leftSide; + + long position { + get => pos; + set { + pos = value; + OnPositionChanged (); + } + } + + /// + /// Initializes a class using layout. + /// + /// The to view and edit as hex, this must support seeking, or an exception will be thrown. + public HexView (Stream source) : base () + { + Source = source; + CanFocus = true; + leftSide = true; + firstNibble = true; + + // Things this view knows how to do + AddCommand (Command.Left, () => MoveLeft ()); + AddCommand (Command.Right, () => MoveRight ()); + AddCommand (Command.LineDown, () => MoveDown (bytesPerLine)); + AddCommand (Command.LineUp, () => MoveUp (bytesPerLine)); + AddCommand (Command.ToggleChecked, () => ToggleSide ()); + AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height)); + AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height)); + AddCommand (Command.TopHome, () => MoveHome ()); + AddCommand (Command.BottomEnd, () => MoveEnd ()); + AddCommand (Command.StartOfLine, () => MoveStartOfLine ()); + AddCommand (Command.EndOfLine, () => MoveEndOfLine ()); + AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine))); + AddCommand (Command.EndOfPage, () => MoveDown (bytesPerLine * (Frame.Height - 1 - (int)(position - displayStart) / bytesPerLine))); + + // Default keybindings for this view + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.Enter, Command.ToggleChecked); + + KeyBindings.Add ('v' + KeyCode.AltMask, Command.PageUp); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + + KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.PageDown); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.StartOfLine); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.EndOfLine); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.StartOfPage); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.EndOfPage); + } + /// - /// An hex viewer and editor over a + /// Initializes a class using layout. /// - /// - /// - /// provides a hex editor on top of a seekable with the left side showing an hex - /// dump of the values in the and the right side showing the contents (filtered to - /// non-control sequence ASCII characters). - /// - /// - /// Users can switch from one side to the other by using the tab key. - /// - /// - /// To enable editing, set to true. When is true - /// the user can make changes to the hexadecimal values of the . Any changes are tracked - /// in the property (a ) indicating - /// the position where the changes were made and the new values. A convenience method, - /// will apply the edits to the . - /// - /// - /// Control the first byte shown by setting the property - /// to an offset in the stream. - /// - /// - public partial class HexView : View { - SortedDictionary edits = new SortedDictionary (); - Stream source; - long displayStart, pos; - bool firstNibble, leftSide; - - private long position { - get => pos; - set { - pos = value; - OnPositionChanged (); + public HexView () : this (source: new MemoryStream ()) { } + + /// + /// Event to be invoked when an edit is made on the . + /// + public event EventHandler Edited; + + /// + /// Event to be invoked when the position and cursor position changes. + /// + public event EventHandler PositionChanged; + + /// + /// Sets or gets the the is operating on; the stream must support seeking ( == true). + /// + /// The source. + public Stream Source { + get => source; + set { + if (value == null) { + throw new ArgumentNullException ("source"); } - } + if (!value.CanSeek) { + throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); + } + source = value; - /// - /// Initializes a class using layout. - /// - /// The to view and edit as hex, this must support seeking, or an exception will be thrown. - public HexView (Stream source) : base () - { - Source = source; - CanFocus = true; - leftSide = true; - firstNibble = true; - - // Things this view knows how to do - AddCommand (Command.Left, () => MoveLeft ()); - AddCommand (Command.Right, () => MoveRight ()); - AddCommand (Command.LineDown, () => MoveDown (bytesPerLine)); - AddCommand (Command.LineUp, () => MoveUp (bytesPerLine)); - AddCommand (Command.ToggleChecked, () => ToggleSide ()); - AddCommand (Command.PageUp, () => MoveUp (bytesPerLine * Frame.Height)); - AddCommand (Command.PageDown, () => MoveDown (bytesPerLine * Frame.Height)); - AddCommand (Command.TopHome, () => MoveHome ()); - AddCommand (Command.BottomEnd, () => MoveEnd ()); - AddCommand (Command.StartOfLine, () => MoveStartOfLine ()); - AddCommand (Command.EndOfLine, () => MoveEndOfLine ()); - AddCommand (Command.StartOfPage, () => MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine))); - AddCommand (Command.EndOfPage, () => MoveDown (bytesPerLine * (Frame.Height - 1 - ((int)(position - displayStart) / bytesPerLine)))); - - // Default keybindings for this view - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.Enter, Command.ToggleChecked); - - AddKeyBinding ('v' + Key.AltMask, Command.PageUp); - AddKeyBinding (Key.PageUp, Command.PageUp); - - AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); - AddKeyBinding (Key.PageDown, Command.PageDown); - - AddKeyBinding (Key.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.StartOfLine); - AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.EndOfLine); - AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.StartOfPage); - AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.EndOfPage); - } - - /// - /// Initializes a class using layout. - /// - public HexView () : this (source: new MemoryStream ()) { } - - /// - /// Event to be invoked when an edit is made on the . - /// - public event EventHandler Edited; - - /// - /// Event to be invoked when the position and cursor position changes. - /// - public event EventHandler PositionChanged; - - /// - /// Sets or gets the the is operating on; the stream must support seeking ( == true). - /// - /// The source. - public Stream Source { - get => source; - set { - if (value == null) - throw new ArgumentNullException ("source"); - if (!value.CanSeek) - throw new ArgumentException ("The source stream must be seekable (CanSeek property)", "source"); - source = value; - - if (displayStart > source.Length) - DisplayStart = 0; - if (position > source.Length) - position = 0; - SetNeedsDisplay (); + if (displayStart > source.Length) { + DisplayStart = 0; + } + if (position > source.Length) { + position = 0; } + SetNeedsDisplay (); } + } - internal void SetDisplayStart (long value) - { - if (value > 0 && value >= source.Length) - displayStart = source.Length - 1; - else if (value < 0) - displayStart = 0; - else - displayStart = value; - SetNeedsDisplay (); + internal void SetDisplayStart (long value) + { + if (value > 0 && value >= source.Length) { + displayStart = source.Length - 1; + } else if (value < 0) { + displayStart = 0; + } else { + displayStart = value; } + SetNeedsDisplay (); + } - /// - /// Sets or gets the offset into the that will displayed at the top of the - /// - /// The display start. - public long DisplayStart { - get => displayStart; - set { - position = value; + /// + /// Sets or gets the offset into the that will displayed at the top of the + /// + /// The display start. + public long DisplayStart { + get => displayStart; + set { + position = value; - SetDisplayStart (value); - } + SetDisplayStart (value); } + } - const int displayWidth = 9; - const int bsize = 4; - int bpl; - private int bytesPerLine { - get => bpl; - set { - bpl = value; - OnPositionChanged (); - } + const int displayWidth = 9; + const int bsize = 4; + int bpl; + + int bytesPerLine { + get => bpl; + set { + bpl = value; + OnPositionChanged (); } + } - /// - public override Rect Frame { - get => base.Frame; - set { - base.Frame = value; + /// + public override Rect Frame { + get => base.Frame; + set { + base.Frame = value; - // Small buffers will just show the position, with the bsize field value (4 bytes) - bytesPerLine = bsize; - if (value.Width - displayWidth > 17) - bytesPerLine = bsize * ((value.Width - displayWidth) / 18); + // Small buffers will just show the position, with the bsize field value (4 bytes) + bytesPerLine = bsize; + if (value.Width - displayWidth > 17) { + bytesPerLine = bsize * ((value.Width - displayWidth) / 18); } } + } - // - // This is used to support editing of the buffer on a peer List<>, - // the offset corresponds to an offset relative to DisplayStart, and - // the buffer contains the contents of a screenful of data, so the - // offset is relative to the buffer. - // - // - byte GetData (byte [] buffer, int offset, out bool edited) - { - var pos = DisplayStart + offset; - if (edits.TryGetValue (pos, out byte v)) { - edited = true; - return v; - } - edited = false; - return buffer [offset]; + // + // This is used to support editing of the buffer on a peer List<>, + // the offset corresponds to an offset relative to DisplayStart, and + // the buffer contains the contents of a screenful of data, so the + // offset is relative to the buffer. + // + // + byte GetData (byte [] buffer, int offset, out bool edited) + { + long pos = DisplayStart + offset; + if (edits.TryGetValue (pos, out byte v)) { + edited = true; + return v; } + edited = false; + return buffer [offset]; + } - /// - public override void OnDrawContent (Rect contentArea) - { - Attribute currentAttribute; - var current = ColorScheme.Focus; - Driver.SetAttribute (current); - Move (0, 0); - - var frame = Frame; - - var nblocks = bytesPerLine / bsize; - var data = new byte [nblocks * bsize * frame.Height]; - Source.Position = displayStart; - var n = source.Read (data, 0, data.Length); - - var activeColor = ColorScheme.HotNormal; - var trackingColor = ColorScheme.HotFocus; - - for (int line = 0; line < frame.Height; line++) { - var lineRect = new Rect (0, line, frame.Width, 1); - if (!Bounds.Contains (lineRect)) - continue; - - Move (0, line); - Driver.SetAttribute (ColorScheme.HotNormal); - Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * bsize)); - - currentAttribute = ColorScheme.HotNormal; - SetAttribute (GetNormalColor ()); - - for (int block = 0; block < nblocks; block++) { - for (int b = 0; b < bsize; b++) { - var offset = (line * nblocks * bsize) + block * bsize + b; - var value = GetData (data, offset, out bool edited); - if (offset + displayStart == position || edited) - SetAttribute (leftSide ? activeColor : trackingColor); - else - SetAttribute (GetNormalColor ()); - - Driver.AddStr (offset >= n && !edited ? " " : string.Format ("{0:x2}", value)); - SetAttribute (GetNormalColor ()); - Driver.AddRune ((Rune)' '); - } - Driver.AddStr (block + 1 == nblocks ? " " : "| "); - } + /// + public override void OnDrawContent (Rect contentArea) + { + Attribute currentAttribute; + var current = ColorScheme.Focus; + Driver.SetAttribute (current); + Move (0, 0); - for (int bitem = 0; bitem < nblocks * bsize; bitem++) { - var offset = line * nblocks * bsize + bitem; - var b = GetData (data, offset, out bool edited); - Rune c; - if (offset >= n && !edited) - c = (Rune)' '; - else { - if (b < 32) - c = (Rune)'.'; - else if (b > 127) - c = (Rune)'.'; - else - Rune.DecodeFromUtf8 (new ReadOnlySpan (b), out c, out _); - } - if (offset + displayStart == position || edited) - SetAttribute (leftSide ? trackingColor : activeColor); - else + var frame = Frame; + + int nblocks = bytesPerLine / bsize; + byte [] data = new byte [nblocks * bsize * frame.Height]; + Source.Position = displayStart; + int n = source.Read (data, 0, data.Length); + + var activeColor = ColorScheme.HotNormal; + var trackingColor = ColorScheme.HotFocus; + + for (int line = 0; line < frame.Height; line++) { + var lineRect = new Rect (0, line, frame.Width, 1); + if (!Bounds.Contains (lineRect)) { + continue; + } + + Move (0, line); + Driver.SetAttribute (ColorScheme.HotNormal); + Driver.AddStr (string.Format ("{0:x8} ", displayStart + line * nblocks * bsize)); + + currentAttribute = ColorScheme.HotNormal; + SetAttribute (GetNormalColor ()); + + for (int block = 0; block < nblocks; block++) { + for (int b = 0; b < bsize; b++) { + int offset = line * nblocks * bsize + block * bsize + b; + byte value = GetData (data, offset, out bool edited); + if (offset + displayStart == position || edited) { + SetAttribute (leftSide ? activeColor : trackingColor); + } else { SetAttribute (GetNormalColor ()); + } - Driver.AddRune (c); + Driver.AddStr (offset >= n && !edited ? " " : string.Format ("{0:x2}", value)); + SetAttribute (GetNormalColor ()); + Driver.AddRune ((Rune)' '); } + Driver.AddStr (block + 1 == nblocks ? " " : "| "); } - void SetAttribute (Attribute attribute) - { - if (currentAttribute != attribute) { - currentAttribute = attribute; - Driver.SetAttribute (attribute); + for (int bitem = 0; bitem < nblocks * bsize; bitem++) { + int offset = line * nblocks * bsize + bitem; + byte b = GetData (data, offset, out bool edited); + Rune c; + if (offset >= n && !edited) { + c = (Rune)' '; + } else { + if (b < 32) { + c = (Rune)'.'; + } else if (b > 127) { + c = (Rune)'.'; + } else { + Rune.DecodeFromUtf8 (new ReadOnlySpan (ref b), out c, out _); + } } + if (offset + displayStart == position || edited) { + SetAttribute (leftSide ? trackingColor : activeColor); + } else { + SetAttribute (GetNormalColor ()); + } + + Driver.AddRune (c); } } - /// - public override void PositionCursor () + void SetAttribute (Attribute attribute) { - var delta = (int)(position - displayStart); - var line = delta / bytesPerLine; - var item = delta % bytesPerLine; - var block = item / bsize; - var column = (item % bsize) * 3; + if (currentAttribute != attribute) { + currentAttribute = attribute; + Driver.SetAttribute (attribute); + } + } + } - if (leftSide) - Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line); - else - Move (displayWidth + (bytesPerLine / bsize) * 14 + item - 1, line); + /// + public override void PositionCursor () + { + int delta = (int)(position - displayStart); + int line = delta / bytesPerLine; + int item = delta % bytesPerLine; + int block = item / bsize; + int column = item % bsize * 3; + + if (leftSide) { + Move (displayWidth + block * 14 + column + (firstNibble ? 0 : 1), line); + } else { + Move (displayWidth + bytesPerLine / bsize * 14 + item - 1, line); } + } - void RedisplayLine (long pos) - { - var delta = (int)(pos - DisplayStart); - var line = delta / bytesPerLine; + void RedisplayLine (long pos) + { + int delta = (int)(pos - DisplayStart); + int line = delta / bytesPerLine; - SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); - } + SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); + } - bool MoveEndOfLine () - { - position = Math.Min ((position / bytesPerLine * bytesPerLine) + bytesPerLine - 1, source.Length); - SetNeedsDisplay (); + bool MoveEndOfLine () + { + position = Math.Min (position / bytesPerLine * bytesPerLine + bytesPerLine - 1, source.Length); + SetNeedsDisplay (); - return true; - } + return true; + } - bool MoveStartOfLine () - { - position = position / bytesPerLine * bytesPerLine; - SetNeedsDisplay (); + bool MoveStartOfLine () + { + position = position / bytesPerLine * bytesPerLine; + SetNeedsDisplay (); - return true; + return true; + } + + bool MoveEnd () + { + position = source.Length; + if (position >= DisplayStart + bytesPerLine * Frame.Height) { + SetDisplayStart (position); + SetNeedsDisplay (); + } else { + RedisplayLine (position); } - bool MoveEnd () - { - position = source.Length; - if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { - SetDisplayStart (position); - SetNeedsDisplay (); - } else - RedisplayLine (position); + return true; + } - return true; - } + bool MoveHome () + { + DisplayStart = 0; + SetNeedsDisplay (); - bool MoveHome () - { - DisplayStart = 0; - SetNeedsDisplay (); + return true; + } - return true; - } + bool ToggleSide () + { + leftSide = !leftSide; + RedisplayLine (position); + firstNibble = true; - bool ToggleSide () - { - leftSide = !leftSide; - RedisplayLine (position); - firstNibble = true; + return true; + } + bool MoveLeft () + { + RedisplayLine (position); + if (leftSide) { + if (!firstNibble) { + firstNibble = true; + return true; + } + firstNibble = false; + } + if (position == 0) { return true; } - - bool MoveLeft () - { + if (position - 1 < DisplayStart) { + SetDisplayStart (displayStart - bytesPerLine); + SetNeedsDisplay (); + } else { RedisplayLine (position); - if (leftSide) { - if (!firstNibble) { - firstNibble = true; - return true; - } + } + position--; + + return true; + } + + bool MoveRight () + { + RedisplayLine (position); + if (leftSide) { + if (firstNibble) { firstNibble = false; - } - if (position == 0) return true; - if (position - 1 < DisplayStart) { - SetDisplayStart (displayStart - bytesPerLine); - SetNeedsDisplay (); - } else - RedisplayLine (position); - position--; - - return true; + } else { + firstNibble = true; + } } - - bool MoveRight () - { + if (position < source.Length) { + position++; + } + if (position >= DisplayStart + bytesPerLine * Frame.Height) { + SetDisplayStart (DisplayStart + bytesPerLine); + SetNeedsDisplay (); + } else { RedisplayLine (position); - if (leftSide) { - if (firstNibble) { - firstNibble = false; - return true; - } else - firstNibble = true; - } - if (position < source.Length) - position++; - if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { - SetDisplayStart (DisplayStart + bytesPerLine); - SetNeedsDisplay (); - } else - RedisplayLine (position); + } - return true; + return true; + } + + bool MoveUp (int bytes) + { + RedisplayLine (position); + if (position - bytes > -1) { + position -= bytes; + } + if (position < DisplayStart) { + SetDisplayStart (DisplayStart - bytes); + SetNeedsDisplay (); + } else { + RedisplayLine (position); } - bool MoveUp (int bytes) - { + return true; + } + + bool MoveDown (int bytes) + { + 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) { + long p = position; + while (p + bytesPerLine < source.Length) { + p += bytesPerLine; + } + position = p; + } + if (position >= DisplayStart + bytesPerLine * Frame.Height) { + SetDisplayStart (DisplayStart + bytes); + SetNeedsDisplay (); + } else { RedisplayLine (position); - if (position - bytes > -1) - position -= bytes; - if (position < DisplayStart) { - SetDisplayStart (DisplayStart - bytes); - SetNeedsDisplay (); - } else - RedisplayLine (position); + } - return true; + return true; + } + + /// + public override bool OnProcessKeyDown (Key keyEvent) + { + if (!AllowEdits) { + return false; } - bool MoveDown (int bytes) - { - 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))) { - var p = position; - while (p + bytesPerLine < source.Length) { - p += bytesPerLine; - } - position = p; + // Ignore control characters and other special keys + if (keyEvent.KeyCode < KeyCode.Space || keyEvent.KeyCode > KeyCode.CharMask) { + return false; + } + + if (leftSide) { + int value; + char k = (char)keyEvent.KeyCode; + if (k >= 'A' && k <= 'F') { + value = k - 'A' + 10; + } else if (k >= 'a' && k <= 'f') { + value = k - 'a' + 10; + } else if (k >= '0' && k <= '9') { + value = k - '0'; + } else { + return false; } - if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { - SetDisplayStart (DisplayStart + bytes); - SetNeedsDisplay (); - } else - RedisplayLine (position); + byte b; + if (!edits.TryGetValue (position, out b)) { + source.Position = position; + b = (byte)source.ReadByte (); + } + RedisplayLine (position); + if (firstNibble) { + firstNibble = false; + b = (byte)(b & 0xf | value << bsize); + edits [position] = b; + OnEdited (new HexViewEditEventArgs (position, edits [position])); + } else { + b = (byte)(b & 0xf0 | value); + edits [position] = b; + OnEdited (new HexViewEditEventArgs (position, edits [position])); + MoveRight (); + } return true; + } else { + return false; } + } - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; - - if (!AllowEdits) - return false; + /// + /// Method used to invoke the event passing the . + /// + /// The key value pair. + public virtual void OnEdited (HexViewEditEventArgs e) + { + Edited?.Invoke (this, e); + } - // Ignore control characters and other special keys - if (keyEvent.Key < Key.Space || keyEvent.Key > Key.CharMask) - return false; + /// + /// Method used to invoke the event passing the arguments. + /// + public virtual void OnPositionChanged () + { + PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); + } - if (leftSide) { - int value; - var k = (char)keyEvent.Key; - if (k >= 'A' && k <= 'F') - value = k - 'A' + 10; - else if (k >= 'a' && k <= 'f') - value = k - 'a' + 10; - else if (k >= '0' && k <= '9') - value = k - '0'; - else - return false; - - byte b; - if (!edits.TryGetValue (position, out b)) { - source.Position = position; - b = (byte)source.ReadByte (); - } - RedisplayLine (position); - if (firstNibble) { - firstNibble = false; - b = (byte)(b & 0xf | (value << bsize)); - edits [position] = b; - OnEdited (new HexViewEditEventArgs (position, edits [position])); - } else { - b = (byte)(b & 0xf0 | value); - edits [position] = b; - OnEdited (new HexViewEditEventArgs (position, edits [position])); - MoveRight (); - } - return true; - } else - return false; + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) + && !me.Flags.HasFlag (MouseFlags.WheeledDown) && !me.Flags.HasFlag (MouseFlags.WheeledUp)) { + return false; } - /// - /// Method used to invoke the event passing the . - /// - /// The key value pair. - public virtual void OnEdited (HexViewEditEventArgs e) - { - Edited?.Invoke (this, e); + if (!HasFocus) { + SetFocus (); } - /// - /// Method used to invoke the event passing the arguments. - /// - public virtual void OnPositionChanged () - { - PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); + if (me.Flags == MouseFlags.WheeledDown) { + DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length); + return true; } - /// - public override bool MouseEvent (MouseEvent me) - { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) - && !me.Flags.HasFlag (MouseFlags.WheeledDown) && !me.Flags.HasFlag (MouseFlags.WheeledUp)) - return false; + if (me.Flags == MouseFlags.WheeledUp) { + DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0); + return true; + } - if (!HasFocus) - SetFocus (); + if (me.X < displayWidth) { + return true; + } + int nblocks = bytesPerLine / bsize; + int blocksSize = nblocks * 14; + int blocksRightOffset = displayWidth + blocksSize - 1; + if (me.X > blocksRightOffset + bytesPerLine - 1) { + return true; + } + leftSide = me.X >= blocksRightOffset; + long lineStart = me.Y * bytesPerLine + displayStart; + int x = me.X - displayWidth + 1; + int block = x / 14; + x -= block * 2; + int empty = x % 3; + int item = x / 3; + if (!leftSide && item > 0 && (empty == 0 || x == block * 14 + 14 - 1 - block * 2)) { + return true; + } + firstNibble = true; + if (leftSide) { + position = Math.Min (lineStart + me.X - blocksRightOffset, source.Length); + } else { + position = Math.Min (lineStart + item, source.Length); + } - if (me.Flags == MouseFlags.WheeledDown) { - DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length); - return true; + if (me.Flags == MouseFlags.Button1DoubleClicked) { + leftSide = !leftSide; + if (leftSide) { + firstNibble = empty == 1; + } else { + firstNibble = true; } + } + SetNeedsDisplay (); - if (me.Flags == MouseFlags.WheeledUp) { - DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0); - return true; - } + return true; + } - if (me.X < displayWidth) - return true; - var nblocks = bytesPerLine / bsize; - var blocksSize = nblocks * 14; - var blocksRightOffset = displayWidth + blocksSize - 1; - if (me.X > blocksRightOffset + bytesPerLine - 1) - return true; - leftSide = me.X >= blocksRightOffset; - var lineStart = (me.Y * bytesPerLine) + displayStart; - var x = me.X - displayWidth + 1; - var block = x / 14; - x -= block * 2; - var empty = x % 3; - var item = x / 3; - if (!leftSide && item > 0 && (empty == 0 || x == (block * 14) + 14 - 1 - (block * 2))) - return true; - firstNibble = true; - if (leftSide) - position = Math.Min (lineStart + me.X - blocksRightOffset, source.Length); - else - position = Math.Min (lineStart + item, source.Length); - - if (me.Flags == MouseFlags.Button1DoubleClicked) { - leftSide = !leftSide; - if (leftSide) - firstNibble = empty == 1; - else - firstNibble = true; - } - SetNeedsDisplay (); + /// + /// Gets or sets whether this allow editing of the + /// of the underlying . + /// + /// true if allow edits; otherwise, false. + public bool AllowEdits { get; set; } = true; - return true; - } + /// + /// Gets a describing the edits done to the . + /// Each Key indicates an offset where an edit was made and the Value is the changed byte. + /// + /// The edits. + public IReadOnlyDictionary Edits => edits; - /// - /// Gets or sets whether this allow editing of the - /// of the underlying . - /// - /// true if allow edits; otherwise, false. - public bool AllowEdits { get; set; } = true; - - /// - /// Gets a describing the edits done to the . - /// Each Key indicates an offset where an edit was made and the Value is the changed byte. - /// - /// The edits. - public IReadOnlyDictionary Edits => edits; - - /// - /// Gets the current character position starting at one, related to the . - /// - public long Position => position + 1; - - /// - /// Gets the current cursor position starting at one for both, line and column. - /// - public Point CursorPosition { - get { - var delta = (int)position; - var line = delta / bytesPerLine + 1; - var item = delta % bytesPerLine + 1; - - return new Point (item, line); - } - } + /// + /// Gets the current character position starting at one, related to the . + /// + public long Position => position + 1; - /// - /// The bytes length per line. - /// - public int BytesPerLine => bytesPerLine; + /// + /// Gets the current cursor position starting at one for both, line and column. + /// + public Point CursorPosition { + get { + int delta = (int)position; + int line = delta / bytesPerLine + 1; + int item = delta % bytesPerLine + 1; - /// - /// This method applies and edits made to the and resets the - /// contents of the property. - /// - /// If provided also applies the changes to the passed . - public void ApplyEdits (Stream stream = null) - { - foreach (var kv in edits) { - source.Position = kv.Key; - source.WriteByte (kv.Value); - source.Flush (); - if (stream != null) { - stream.Position = kv.Key; - stream.WriteByte (kv.Value); - stream.Flush (); - } - } - edits = new SortedDictionary (); - SetNeedsDisplay (); + return new Point (item, line); } + } - /// - /// This method discards the edits made to the by resetting the - /// contents of the property. - /// - public void DiscardEdits () - { - edits = new SortedDictionary (); + /// + /// The bytes length per line. + /// + public int BytesPerLine => bytesPerLine; + + /// + /// This method applies and edits made to the and resets the + /// contents of the property. + /// + /// If provided also applies the changes to the passed . + public void ApplyEdits (Stream stream = null) + { + foreach (var kv in edits) { + source.Position = kv.Key; + source.WriteByte (kv.Value); + source.Flush (); + if (stream != null) { + stream.Position = kv.Key; + stream.WriteByte (kv.Value); + stream.Flush (); + } } + edits = new SortedDictionary (); + SetNeedsDisplay (); + } - private CursorVisibility desiredCursorVisibility = CursorVisibility.Default; + /// + /// This method discards the edits made to the by resetting the + /// contents of the property. + /// + public void DiscardEdits () + { + edits = new SortedDictionary (); + } - /// - /// Get / Set the wished cursor when the field is focused - /// - public CursorVisibility DesiredCursorVisibility { - get => desiredCursorVisibility; - set { - if (desiredCursorVisibility != value && HasFocus) { - Application.Driver.SetCursorVisibility (value); - } + CursorVisibility desiredCursorVisibility = CursorVisibility.Default; - desiredCursorVisibility = value; + /// + /// Get / Set the wished cursor when the field is focused + /// + public CursorVisibility DesiredCursorVisibility { + get => desiredCursorVisibility; + set { + if (desiredCursorVisibility != value && HasFocus) { + Application.Driver.SetCursorVisibility (value); } + + desiredCursorVisibility = value; } + } - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (DesiredCursorVisibility); + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (DesiredCursorVisibility); - return base.OnEnter (view); - } + return base.OnEnter (view); } -} +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Label.cs b/Terminal.Gui/Views/Label.cs index 29b476fb77..40350e9c57 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -58,12 +58,31 @@ void SetInitialProperties (bool autosize = true) { Height = 1; AutoSize = autosize; - //HotKeySpecifier = new Rune ('_'); - //if (HotKey != Key.Null) { - // AddKeyBinding (Key.Space | HotKey, Command.Accept); - //} + // Things this view knows how to do + AddCommand (Command.Default, () => { + // BUGBUG: This is a hack, but it does work. + var can = CanFocus; + CanFocus = true; + SetFocus (); + SuperView.FocusNext (); + CanFocus = can; + return true; + }); + AddCommand (Command.Accept, () => AcceptKey ()); + + // Default key bindings for this view + KeyBindings.Add (KeyCode.Space, Command.Accept); } + bool AcceptKey () + { + if (!HasFocus) { + SetFocus (); + } + OnClicked (); + return true; + } + /// /// The event fired when the user clicks the primary mouse button within the Bounds of this /// or if the user presses the action key while this view is focused. (TODO: IsDefault) @@ -111,19 +130,6 @@ public override bool OnEnter (View view) return base.OnEnter (view); } - /// - public override bool ProcessHotKey (KeyEvent ke) - { - if (ke.Key == (Key.AltMask | HotKey)) { - if (!HasFocus) { - SetFocus (); - } - OnClicked (); - return true; - } - return base.ProcessHotKey (ke); - } - /// /// Virtual method to invoke the event. /// diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index a5dd1ed7ef..5e57efbe99 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -166,9 +166,9 @@ public bool AllowsMarking { set { allowsMarking = value; if (allowsMarking) { - AddKeyBinding (Key.Space, Command.ToggleChecked); + KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); } else { - ClearKeyBinding (Key.Space); + KeyBindings.Remove (KeyCode.Space); } SetNeedsDisplay (); @@ -330,22 +330,22 @@ void Initialize () AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ()); // Default keybindings for all ListViews - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.P | KeyCode.CtrlMask, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.N | KeyCode.CtrlMask, Command.LineDown); - AddKeyBinding (Key.PageUp, Command.PageUp); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.PageDown); - AddKeyBinding (Key.Home, Command.TopHome); + KeyBindings.Add (KeyCode.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); - AddKeyBinding (Key.Enter, Command.OpenSelectedItem); + KeyBindings.Add (KeyCode.Enter, Command.OpenSelectedItem); } /// @@ -416,20 +416,11 @@ public override void OnDrawContent (Rect contentArea) public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); /// - public override bool ProcessKey (KeyEvent kb) + public override bool OnProcessKeyDown (Key a) { - if (source == null) { - return base.ProcessKey (kb); - } - - var result = InvokeKeybindings (kb); - if (result != null) { - return (bool)result; - } - // Enable user to find & select an item by typing text - if (CollectionNavigator.IsCompatibleKey (kb)) { - var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)kb.KeyValue); + if (CollectionNavigator.IsCompatibleKey (a)) { + var newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)a); if (newItem is int && newItem != -1) { SelectedItem = (int)newItem; EnsureSelectedItemVisible (); @@ -437,7 +428,6 @@ public override bool ProcessKey (KeyEvent kb) return true; } } - return false; } diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs deleted file mode 100644 index da8b55023f..0000000000 --- a/Terminal.Gui/Views/Menu.cs +++ /dev/null @@ -1,2215 +0,0 @@ -using System; -using System.Text; -using System.Linq; -using System.Collections.Generic; - -namespace Terminal.Gui { - - /// - /// Specifies how a shows selection state. - /// - [Flags] - public enum MenuItemCheckStyle { - /// - /// The menu item will be shown normally, with no check indicator. The default. - /// - NoCheck = 0b_0000_0000, - - /// - /// The menu item will indicate checked/un-checked state (see ). - /// - Checked = 0b_0000_0001, - - /// - /// The menu item is part of a menu radio group (see ) and will indicate selected state. - /// - Radio = 0b_0000_0010, - }; - - /// - /// A has title, an associated help text, and an action to execute on activation. - /// MenuItems can also have a checked indicator (see ). - /// - public class MenuItem { - string title; - ShortcutHelper shortcutHelper; - bool allowNullChecked; - MenuItemCheckStyle checkType; - - internal int TitleLength => GetMenuBarItemLength (Title); - - /// - /// Gets or sets arbitrary data for the menu item. - /// - /// This property is not used internally. - public object Data { get; set; } - - /// - /// Initializes a new instance of - /// - public MenuItem (Key shortcut = Key.Null) : this ("", "", null, null, null, shortcut) { } - - /// - /// Initializes a new instance of . - /// - /// Title for the menu item. - /// Help text to display. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The of this menu item. - /// The keystroke combination. - public MenuItem (string title, string help, Action action, Func canExecute = null, MenuItem parent = null, Key shortcut = Key.Null) - { - Title = title ?? ""; - Help = help ?? ""; - Action = action; - CanExecute = canExecute; - Parent = parent; - shortcutHelper = new ShortcutHelper (); - if (shortcut != Key.Null) { - shortcutHelper.Shortcut = shortcut; - } - } - - /// - /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the - /// of a MenuItem with an underscore ('_'). - /// - /// Pressing Alt-Hotkey for a (menu items on the menu bar) works even if the menu is not active). - /// Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem. - /// - /// - /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the File menu. - /// Pressing the N key will then activate the New MenuItem. - /// - /// - /// See also which enable global key-bindings to menu items. - /// - /// - public Rune HotKey; - - /// - /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the that is - /// the parent of the or this . - /// - /// The will be drawn on the MenuItem to the right of the and text. See . - /// - /// - public Key Shortcut { - get => shortcutHelper.Shortcut; - set { - if (shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == Key.Null)) { - shortcutHelper.Shortcut = value; - } - } - } - - /// - /// Gets the text describing the keystroke combination defined by . - /// - public string ShortcutTag => ShortcutHelper.GetShortcutTag (shortcutHelper.Shortcut); - - /// - /// Gets or sets the title of the menu item . - /// - /// The title. - public string Title { - get { return title; } - set { - if (title != value) { - title = value; - GetHotKey (); - } - } - } - - /// - /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . - /// - /// The help text. - public string Help { get; set; } - - /// - /// Gets or sets the action to be invoked when the menu item is triggered. - /// - /// Method to invoke. - public Action Action { get; set; } - - /// - /// Gets or sets the action to be invoked to determine if the menu can be triggered. If returns - /// the menu 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; } - - /// - /// Returns if the menu item is enabled. This method is a wrapper around . - /// - public bool IsEnabled () - { - return CanExecute == null ? true : CanExecute (); - } - - // - // ┌─────────────────────────────┐ - // │ Quit Quit UI Catalog Ctrl+Q │ - // └─────────────────────────────┘ - // ┌─────────────────┐ - // │ ◌ TopLevel Alt+T │ - // └─────────────────┘ - // TODO: Replace the `2` literals with named constants - internal int Width => 1 + // space before Title - TitleLength + - 2 + // space after Title - BUGBUG: This should be 1 - (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space - (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0) + // Two spaces before Help - (ShortcutTag.GetColumns () > 0 ? 2 + ShortcutTag.GetColumns () : 0); // Pad two spaces before shortcut tag (which are also aligned right) - - /// - /// Sets or gets whether the shows a check indicator or not. See . - /// - public bool? Checked { set; get; } - - /// - /// Used only if is of type. - /// If allows to be null, true or false. - /// If only allows to be true or false. - /// - public bool AllowNullChecked { - get => allowNullChecked; - set { - allowNullChecked = value; - if (Checked == null) { - Checked = false; - } - } - } - - /// - /// Sets or gets the of a menu item where is set to . - /// - public MenuItemCheckStyle CheckType { - get => checkType; - set { - checkType = value; - if (checkType == MenuItemCheckStyle.Checked && !allowNullChecked && Checked == null) { - Checked = false; - } - } - } - - /// - /// Gets the parent for this . - /// - /// The parent. - public MenuItem Parent { get; set; } - - /// - /// Gets if this is from a sub-menu. - /// - internal bool IsFromSubMenu { get { return Parent != null; } } - - /// - /// Merely a debugging aid to see the interaction with main. - /// - public MenuItem GetMenuItem () - { - return this; - } - - /// - /// Merely a debugging aid to see the interaction with main. - /// - public bool GetMenuBarItem () - { - return IsFromSubMenu; - } - - /// - /// Toggle the between three states if is - /// or between two states if is . - /// - public void ToggleChecked () - { - if (checkType != MenuItemCheckStyle.Checked) { - throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!"); - } - var previousChecked = Checked; - if (AllowNullChecked) { - switch (previousChecked) { - case null: - Checked = true; - break; - case true: - Checked = false; - break; - case false: - Checked = null; - break; - } - } else { - Checked = !Checked; - } - } - - void GetHotKey () - { - bool nextIsHot = false; - foreach (var x in title) { - if (x == MenuBar.HotKeySpecifier.Value) { - nextIsHot = true; - } else { - if (nextIsHot) { - HotKey = (Rune)Char.ToUpper ((char)x); - break; - } - nextIsHot = false; - HotKey = default; - } - } - } - - int GetMenuBarItemLength (string title) - { - int len = 0; - foreach (var ch in title.EnumerateRunes ()) { - if (ch == MenuBar.HotKeySpecifier) - continue; - len += Math.Max (ch.GetColumns (), 1); - } - - return len; - } - } - - /// - /// is a menu item on an app's . - /// MenuBarItems do not support . - /// - public class MenuBarItem : MenuItem { - /// - /// Initializes a new as a . - /// - /// Title for the menu item. - /// Help text to display. Will be displayed next to the Title surrounded by parentheses. - /// Action to invoke when the menu item is activated. - /// Function to determine if the action can currently be executed. - /// The parent of this if exist, otherwise is null. - public MenuBarItem (string title, string help, Action action, Func canExecute = null, MenuItem parent = null) : base (title, help, action, canExecute, parent) - { - Initialize (title, null, null, true); - } - - /// - /// Initializes a new . - /// - /// Title for the menu item. - /// The items in the current menu. - /// The parent of this if exist, otherwise is null. - public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) - { - Initialize (title, children, parent); - } - - /// - /// Initializes a new with separate list of items. - /// - /// Title for the menu item. - /// The list of items in the current menu. - /// The parent of this if exist, otherwise is null. - public MenuBarItem (string title, List children, MenuItem parent = null) - { - Initialize (title, children, parent); - } - - /// - /// Initializes a new . - /// - /// The items in the current menu. - public MenuBarItem (MenuItem [] children) : this ("", children) { } - - /// - /// Initializes a new . - /// - public MenuBarItem () : this (children: new MenuItem [] { }) { } - - void Initialize (string title, object children, MenuItem parent = null, bool isTopLevel = false) - { - if (!isTopLevel && children == null) { - throw new ArgumentNullException (nameof (children), "The parameter cannot be null. Use an empty array instead."); - } - SetTitle (title ?? ""); - if (parent != null) { - Parent = parent; - } - if (children is List) { - MenuItem [] childrens = new MenuItem [] { }; - foreach (var item in (List)children) { - for (int i = 0; i < item.Length; i++) { - SetChildrensParent (item); - Array.Resize (ref childrens, childrens.Length + 1); - childrens [childrens.Length - 1] = item [i]; - } - } - Children = childrens; - } else if (children is MenuItem []) { - SetChildrensParent ((MenuItem [])children); - Children = (MenuItem [])children; - } else { - Children = null; - } - } - - void SetChildrensParent (MenuItem [] childrens) - { - foreach (var child in childrens) { - if (child != null && child.Parent == null) { - child.Parent = this; - } - } - } - - /// - /// Check if the children parameter is a . - /// - /// - /// Returns a or null otherwise. - public MenuBarItem SubMenu (MenuItem children) - { - return children as MenuBarItem; - } - - /// - /// Check if the parameter is a child of this. - /// - /// - /// Returns true if it is a child of this. false otherwise. - public bool IsSubMenuOf (MenuItem menuItem) - { - foreach (var child in Children) { - if (child == menuItem && child.Parent == menuItem.Parent) { - return true; - } - } - return false; - } - - /// - /// Get the index of the parameter. - /// - /// - /// Returns a value bigger than -1 if the is a child of this. - public int GetChildrenIndex (MenuItem children) - { - if (Children?.Length == 0) { - return -1; - } - int i = 0; - foreach (var child in Children) { - if (child == children) { - return i; - } - i++; - } - return -1; - } - - void SetTitle (string title) - { - if (title == null) - title = string.Empty; - Title = title; - } - - /// - /// Gets or sets an array of objects that are the children of this - /// - /// The children. - public MenuItem [] Children { get; set; } - - internal bool IsTopLevel { get => Parent == null && (Children == null || Children.Length == 0) && Action != null; } - } - - class Menu : View { - internal MenuBarItem barItems; - internal MenuBar host; - internal int current; - internal View previousSubFocused; - - internal static Rect MakeFrame (int x, int y, MenuItem [] items, Menu parent = null, LineStyle border = LineStyle.Single) - { - if (items == null || items.Length == 0) { - return new Rect (); - } - int minX = x; - int minY = y; - var borderOffset = 2; // This 2 is for the space around - int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; - int maxH = items.Length + borderOffset; - if (parent != null && x + maxW > Driver.Cols) { - minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); - } - if (y + maxH > Driver.Rows) { - minY = Math.Max (Driver.Rows - maxH, 0); - } - return new Rect (minX, minY, maxW, maxH); - } - - public Menu (MenuBar host, int x, int y, MenuBarItem barItems, Menu parent = null, LineStyle border = LineStyle.Single) - : base (MakeFrame (x, y, barItems.Children, parent, border)) - { - this.barItems = barItems; - this.host = host; - if (barItems.IsTopLevel) { - // This is a standalone MenuItem on a MenuBar - ColorScheme = host.ColorScheme; - CanFocus = true; - } else { - - current = -1; - for (int i = 0; i < barItems.Children?.Length; i++) { - if (barItems.Children [i]?.IsEnabled () == true) { - current = i; - break; - } - } - ColorScheme = host.ColorScheme; - CanFocus = true; - WantMousePositionReports = host.WantMousePositionReports; - } - - BorderStyle = host.MenusBorderStyle; - - if (Application.Current != null) { - Application.Current.DrawContentComplete += Current_DrawContentComplete; - Application.Current.SizeChanging += Current_TerminalResized; - } - Application.MouseEvent += Application_RootMouseEvent; - - // Things this view knows how to do - AddCommand (Command.LineUp, () => MoveUp ()); - AddCommand (Command.LineDown, () => MoveDown ()); - AddCommand (Command.Left, () => { this.host.PreviousMenu (true); return true; }); - AddCommand (Command.Right, () => { - this.host.NextMenu (!this.barItems.IsTopLevel || (this.barItems.Children != null - && this.barItems.Children.Length > 0 && current > -1 - && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu), - this.barItems.Children != null && this.barItems.Children.Length > 0 && current > -1 - && host.UseSubMenusSingleFrame && this.barItems.SubMenu (this.barItems.Children [current]) != null); - - return true; - }); - AddCommand (Command.Cancel, () => { CloseAllMenus (); return true; }); - AddCommand (Command.Accept, () => { RunSelected (); return true; }); - - // Default keybindings for this view - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.Esc, Command.Cancel); - AddKeyBinding (Key.Enter, Command.Accept); - } - - private void Current_TerminalResized (object sender, SizeChangedEventArgs e) - { - if (host.IsMenuOpen) { - host.CloseAllMenus (); - } - } - - /// - public override void OnVisibleChanged () - { - base.OnVisibleChanged (); - if (Visible) { - Application.MouseEvent += Application_RootMouseEvent; - } else { - Application.MouseEvent -= Application_RootMouseEvent; - } - } - - private void Application_RootMouseEvent (object sender, MouseEventEventArgs a) - { - if (a.MouseEvent.View is MenuBar) { - return; - } - var locationOffset = host.GetScreenOffsetFromCurrent (); - if (SuperView != null && SuperView != Application.Current) { - locationOffset.X += SuperView.Border.Thickness.Left; - locationOffset.Y += SuperView.Border.Thickness.Top; - } - var view = View.FindDeepestView (this, a.MouseEvent.X + locationOffset.X, a.MouseEvent.Y + locationOffset.Y, out int rx, out int ry); - if (view == this) { - if (!Visible) { - throw new InvalidOperationException ("This shouldn't running on a invisible menu!"); - } - - var nme = new MouseEvent () { - X = rx, - Y = ry, - Flags = a.MouseEvent.Flags, - View = view - }; - if (MouseEvent (nme) || a.MouseEvent.Flags == MouseFlags.Button1Pressed || a.MouseEvent.Flags == MouseFlags.Button1Released) { - a.MouseEvent.Handled = true; - } - } - } - - internal Attribute DetermineColorSchemeFor (MenuItem item, int index) - { - if (item != null) { - if (index == current) return ColorScheme.Focus; - if (!item.IsEnabled ()) return ColorScheme.Disabled; - } - return GetNormalColor (); - } - - public override void OnDrawContent (Rect contentArea) - { - if (barItems.Children == null) { - return; - } - var savedClip = Driver.Clip; - Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows); - Driver.SetAttribute (GetNormalColor ()); - - OnDrawFrames (); - OnRenderLineCanvas (); - - for (int i = Bounds.Y; i < barItems.Children.Length; i++) { - if (i < 0) { - continue; - } - if (BoundsToScreen (Bounds).Y + i >= Driver.Rows) { - break; - } - var item = barItems.Children [i]; - Driver.SetAttribute (item == null ? GetNormalColor () - : i == current ? ColorScheme.Focus : GetNormalColor ()); - if (item == null && BorderStyle != LineStyle.None) { - Move (-1, i); - Driver.AddRune (CM.Glyphs.LeftTee); - } else if (Frame.X < Driver.Cols) { - Move (0, i); - } - - Driver.SetAttribute (DetermineColorSchemeFor (item, i)); - for (int p = Bounds.X; p < Frame.Width - 2; p++) { // This - 2 is for the border - if (p < 0) { - continue; - } - if (BoundsToScreen (Bounds).X + p >= Driver.Cols) { - break; - } - if (item == null) - Driver.AddRune (CM.Glyphs.HLine); - else if (i == 0 && p == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) - Driver.AddRune (CM.Glyphs.LeftArrow); - // This `- 3` is left border + right border + one row in from right - else if (p == Frame.Width - 3 && barItems.SubMenu (barItems.Children [i]) != null) - Driver.AddRune (CM.Glyphs.RightArrow); - else - Driver.AddRune ((Rune)' '); - } - - if (item == null) { - if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) { - Move (Frame.Width - 2, i); - Driver.AddRune (CM.Glyphs.RightTee); - } - continue; - } - - string textToDraw = null; - var nullCheckedChar = CM.Glyphs.NullChecked; - var checkChar = CM.Glyphs.Selected; - var uncheckedChar = CM.Glyphs.UnSelected; - - if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) { - checkChar = CM.Glyphs.Checked; - uncheckedChar = CM.Glyphs.UnChecked; - } - - // Support Checked even though CheckType wasn't set - if (item.CheckType == MenuItemCheckStyle.Checked && item.Checked == null) { - textToDraw = $"{nullCheckedChar} {item.Title}"; - } else if (item.Checked == true) { - textToDraw = $"{checkChar} {item.Title}"; - } else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) { - textToDraw = $"{uncheckedChar} {item.Title}"; - } else { - textToDraw = item.Title; - } - - BoundsToScreen (0, i, out int vtsCol, out int vtsRow, false); - if (vtsCol < Driver.Cols) { - Driver.Move (vtsCol + 1, vtsRow); - if (!item.IsEnabled ()) { - DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled); - } else if (i == 0 && host.UseSubMenusSingleFrame && item.Parent.Parent != null) { - var tf = new TextFormatter () { - Alignment = TextAlignment.Centered, - HotKeySpecifier = MenuBar.HotKeySpecifier, - Text = textToDraw - }; - // The -3 is left/right border + one space (not sure what for) - tf.Draw (BoundsToScreen (new Rect (1, i, Frame.Width - 3, 1)), - i == current ? ColorScheme.Focus : GetNormalColor (), - i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, - SuperView == null ? default : SuperView.BoundsToScreen (SuperView.Bounds)); - } else { - DrawHotString (textToDraw, - i == current ? ColorScheme.HotFocus : ColorScheme.HotNormal, - i == current ? ColorScheme.Focus : GetNormalColor ()); - } - - // The help string - var l = item.ShortcutTag.GetColumns () == 0 ? item.Help.GetColumns () : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; - var col = Frame.Width - l - 3; - BoundsToScreen (col, i, out vtsCol, out vtsRow, false); - if (vtsCol < Driver.Cols) { - Driver.Move (vtsCol, vtsRow); - Driver.AddStr (item.Help); - - // The shortcut tag string - if (!string.IsNullOrEmpty (item.ShortcutTag)) { - Driver.Move (vtsCol + l - item.ShortcutTag.GetColumns (), vtsRow); - Driver.AddStr (item.ShortcutTag); - } - } - } - } - Driver.Clip = savedClip; - - PositionCursor (); - } - - private void Current_DrawContentComplete (object sender, DrawEventArgs e) - { - if (Visible) { - OnDrawContent (Bounds); - } - } - - public override void PositionCursor () - { - if (host == null || host.IsMenuOpen) - if (barItems.IsTopLevel) { - host.PositionCursor (); - } else - Move (2, 1 + current); - else - host.PositionCursor (); - } - - public void Run (Action action) - { - if (action == null || host == null) - return; - - Application.UngrabMouse (); - host.CloseAllMenus (); - Application.Refresh (); - - host.Run (action); - } - - public override bool OnLeave (View view) - { - return host.OnLeave (view); - } - - public override bool OnKeyDown (KeyEvent keyEvent) - { - if (keyEvent.IsAlt) { - host.CloseAllMenus (); - return true; - } - - return false; - } - - public override bool ProcessHotKey (KeyEvent keyEvent) - { - // To ncurses simulate a AltMask key pressing Alt+Space because - // it can't detect an alone special key down was pressed. - if (keyEvent.IsAlt && keyEvent.Key == Key.AltMask) { - OnKeyDown (keyEvent); - return true; - } - - return false; - } - - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - // TODO: rune-ify - if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) { - var x = Char.ToUpper ((char)kb.KeyValue); - var idx = -1; - foreach (var item in barItems.Children) { - idx++; - if (item == null) continue; - if (item.IsEnabled () && item.HotKey.Value == x) { - current = idx; - RunSelected (); - return true; - } - } - } - return host.ProcessHotKey (kb); - } - - void RunSelected () - { - if (barItems.IsTopLevel) { - Run (barItems.Action); - } else if (current > -1 && barItems.Children [current].Action != null) { - Run (barItems.Children [current].Action); - } else if (current == 0 && host.UseSubMenusSingleFrame - && barItems.Children [current].Parent.Parent != null) { - - host.PreviousMenu (barItems.Children [current].Parent.IsFromSubMenu, true); - } else if (current > -1 && barItems.SubMenu (barItems.Children [current]) != null) { - - CheckSubMenu (); - } - } - - void CloseAllMenus () - { - Application.UngrabMouse (); - host.CloseAllMenus (); - } - - bool MoveDown () - { - if (barItems.IsTopLevel) { - return true; - } - bool disabled; - do { - current++; - if (current >= barItems.Children.Length) { - current = 0; - } - if (this != host.openCurrentMenu && barItems.Children [current]?.IsFromSubMenu == true && host.selectedSub > -1) { - host.PreviousMenu (true); - host.SelectEnabledItem (barItems.Children, current, out current); - host.openCurrentMenu = this; - } - var item = barItems.Children [current]; - if (item?.IsEnabled () != true) { - disabled = true; - } else { - disabled = false; - } - if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && - !disabled && host.IsMenuOpen) { - if (!CheckSubMenu ()) - return false; - break; - } - if (!host.IsMenuOpen) { - host.OpenMenu (host.selected); - } - } while (barItems.Children [current] == null || disabled); - SetNeedsDisplay (); - SetParentSetNeedsDisplay (); - if (!host.UseSubMenusSingleFrame) - host.OnMenuOpened (); - return true; - } - - bool MoveUp () - { - if (barItems.IsTopLevel || current == -1) { - return true; - } - bool disabled; - do { - current--; - if (host.UseKeysUpDownAsKeysLeftRight && !host.UseSubMenusSingleFrame) { - if ((current == -1 || this != host.openCurrentMenu) && barItems.Children [current + 1].IsFromSubMenu && host.selectedSub > -1) { - current++; - host.PreviousMenu (true); - if (current > 0) { - current--; - host.openCurrentMenu = this; - } - break; - } - } - if (current < 0) - current = barItems.Children.Length - 1; - if (!host.SelectEnabledItem (barItems.Children, current, out current, false)) { - current = 0; - if (!host.SelectEnabledItem (barItems.Children, current, out current) && !host.CloseMenu (false)) { - return false; - } - break; - } - var item = barItems.Children [current]; - if (item?.IsEnabled () != true) { - disabled = true; - } else { - disabled = false; - } - if (!host.UseSubMenusSingleFrame && host.UseKeysUpDownAsKeysLeftRight && barItems.SubMenu (barItems.Children [current]) != null && - !disabled && host.IsMenuOpen) { - if (!CheckSubMenu ()) - return false; - break; - } - } while (barItems.Children [current] == null || disabled); - SetNeedsDisplay (); - SetParentSetNeedsDisplay (); - if (!host.UseSubMenusSingleFrame) - host.OnMenuOpened (); - return true; - } - - private void SetParentSetNeedsDisplay () - { - if (host.openSubMenu != null) { - foreach (var menu in host.openSubMenu) { - menu.SetNeedsDisplay (); - } - } - - host?.openMenu?.SetNeedsDisplay (); - host.SetNeedsDisplay (); - } - - public override bool MouseEvent (MouseEvent me) - { - if (!host.handled && !host.HandleGrabView (me, this)) { - return false; - } - host.handled = false; - bool disabled; - var meY = me.Y - (Border == null ? 0 : Border.Thickness.Top); - if (me.Flags == MouseFlags.Button1Clicked) { - disabled = false; - if (meY < 0) - return true; - if (meY >= barItems.Children.Length) - return true; - var item = barItems.Children [meY]; - if (item == null || !item.IsEnabled ()) disabled = true; - if (disabled) return true; - current = meY; - if (item != null && !disabled) - RunSelected (); - return true; - } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || - me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition || - me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { - - disabled = false; - if (meY < 0 || meY >= barItems.Children.Length) { - return true; - } - var item = barItems.Children [meY]; - if (item == null) return true; - if (item == null || !item.IsEnabled ()) disabled = true; - if (item != null && !disabled) - current = meY; - if (host.UseSubMenusSingleFrame || !CheckSubMenu ()) { - SetNeedsDisplay (); - SetParentSetNeedsDisplay (); - return true; - } - host.OnMenuOpened (); - return true; - } - return false; - } - - internal bool CheckSubMenu () - { - if (current == -1 || barItems.Children [current] == null) { - return true; - } - var subMenu = barItems.SubMenu (barItems.Children [current]); - if (subMenu != null) { - int pos = -1; - if (host.openSubMenu != null) { - pos = host.openSubMenu.FindIndex (o => o?.barItems == subMenu); - } - if (pos == -1 && this != host.openCurrentMenu && subMenu.Children != host.openCurrentMenu.barItems.Children - && !host.CloseMenu (false, true)) { - return false; - } - host.Activate (host.selected, pos, subMenu); - } else if (host.openSubMenu?.Count == 0 || host.openSubMenu?.Last ().barItems.IsSubMenuOf (barItems.Children [current]) == false) { - return host.CloseMenu (false, true); - } else { - SetNeedsDisplay (); - SetParentSetNeedsDisplay (); - } - return true; - } - - int GetSubMenuIndex (MenuBarItem subMenu) - { - int pos = -1; - if (this != null && Subviews.Count > 0) { - Menu v = null; - foreach (var menu in Subviews) { - if (((Menu)menu).barItems == subMenu) - v = (Menu)menu; - } - if (v != null) - pos = Subviews.IndexOf (v); - } - - return pos; - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - - return base.OnEnter (view); - } - - protected override void Dispose (bool disposing) - { - if (Application.Current != null) { - Application.Current.DrawContentComplete -= Current_DrawContentComplete; - Application.Current.SizeChanging -= Current_TerminalResized; - } - Application.MouseEvent -= Application_RootMouseEvent; - base.Dispose (disposing); - } - } - - /// - /// - /// Provides a menu bar that spans the top of a View with drop-down and cascading menus. - /// - /// - /// By default, any sub-sub-menus (sub-menus of the s added to s) - /// are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame - /// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting - /// to , this behavior can be changed such that all sub-sub-menus are - /// drawn within a single frame below the MenuBar. - /// - /// - /// - /// - /// The appears on the first row of the parent View and uses the full width. - /// - /// - /// The provides global hotkeys for the application. See . - /// - /// - /// See also: - /// - /// - public class MenuBar : View { - internal int selected; - internal int selectedSub; - - /// - /// Gets or sets the array of s for the menu. Only set this after the is visible. - /// - /// The menu array. - public MenuBarItem [] Menus { get; set; } - - /// - /// The default for 's border. The default is . - /// - public LineStyle MenusBorderStyle { get; set; } = LineStyle.Single; - - private bool useKeysUpDownAsKeysLeftRight = false; - - /// - /// Used for change the navigation key style. - /// - public bool UseKeysUpDownAsKeysLeftRight { - get => useKeysUpDownAsKeysLeftRight; - set { - useKeysUpDownAsKeysLeftRight = value; - if (value && UseSubMenusSingleFrame) { - UseSubMenusSingleFrame = false; - SetNeedsDisplay (); - } - } - } - - static string shortcutDelimiter = "+"; - /// - /// Sets or gets the shortcut delimiter separator. The default is "+". - /// - public static string ShortcutDelimiter { - get => shortcutDelimiter; - set { - if (shortcutDelimiter != value) { - shortcutDelimiter = value == string.Empty ? " " : value; - } - } - } - - /// - /// The specifier character for the hotkey to all menus. - /// - new public static Rune HotKeySpecifier => (Rune)'_'; - - private bool useSubMenusSingleFrame; - - /// - /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. - /// - /// By default any sub-sub-menus (sub-menus of the main s) are displayed in a cascading manner, - /// where each sub-sub-menu pops out of the sub-menu frame - /// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting - /// to , this behavior can be changed such that all sub-sub-menus are - /// drawn within a single frame below the MenuBar. - /// - /// - public bool UseSubMenusSingleFrame { - get => useSubMenusSingleFrame; - set { - useSubMenusSingleFrame = value; - if (value && UseKeysUpDownAsKeysLeftRight) { - useKeysUpDownAsKeysLeftRight = false; - SetNeedsDisplay (); - } - } - } - - /// - /// The used to activate the menu bar by keyboard. - /// - public Key Key { get; set; } = Key.F9; - - /// - public override bool Visible { - get => base.Visible; - set { - base.Visible = value; - if (!value) { - CloseAllMenus (); - } - } - } - - /// - /// Initializes a new instance of the . - /// - public MenuBar () : this (new MenuBarItem [] { }) { } - - /// - /// Initializes a new instance of the class with the specified set of Toplevel menu items. - /// - /// Individual menu items; a null item will result in a separator being drawn. - public MenuBar (MenuBarItem [] menus) : base () - { - X = 0; - Y = 0; - Width = Dim.Fill (); - Height = 1; - Menus = menus; - //CanFocus = true; - selected = -1; - selectedSub = -1; - ColorScheme = Colors.Menu; - WantMousePositionReports = true; - IsMenuOpen = false; - - Added += MenuBar_Added; - - // Things this view knows how to do - AddCommand (Command.Left, () => { MoveLeft (); return true; }); - AddCommand (Command.Right, () => { MoveRight (); return true; }); - AddCommand (Command.Cancel, () => { CloseMenuBar (); return true; }); - AddCommand (Command.Accept, () => { ProcessMenu (selected, Menus [selected]); return true; }); - - // Default keybindings for this view - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.Esc, Command.Cancel); - AddKeyBinding (Key.C | Key.CtrlMask, Command.Cancel); - AddKeyBinding (Key.CursorDown, Command.Accept); - AddKeyBinding (Key.Enter, Command.Accept); - } - - bool _initialCanFocus; - - private void MenuBar_Added (object sender, SuperViewChangedEventArgs e) - { - _initialCanFocus = CanFocus; - Added -= MenuBar_Added; - } - - bool openedByAltKey; - - bool isCleaning; - - /// - public override bool OnLeave (View view) - { - if ((!(view is MenuBar) && !(view is Menu) || !(view is MenuBar) && !(view is Menu) && openMenu != null) && !isCleaning && !reopen) { - CleanUp (); - } - return base.OnLeave (view); - } - - /// - public override bool OnKeyDown (KeyEvent keyEvent) - { - if (keyEvent.IsAlt || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) { - openedByAltKey = true; - SetNeedsDisplay (); - openedByHotKey = false; - } - return false; - } - - /// - public override bool OnKeyUp (KeyEvent keyEvent) - { - if (keyEvent.IsAlt || keyEvent.Key == Key.AltMask || (keyEvent.IsCtrl && keyEvent.Key == (Key.CtrlMask | Key.Space))) { - // User pressed Alt - this may be a precursor to a menu accelerator (e.g. Alt-F) - if (openedByAltKey && !IsMenuOpen && openMenu == null && (((uint)keyEvent.Key & (uint)Key.CharMask) == 0 - || ((uint)keyEvent.Key & (uint)Key.CharMask) == (uint)Key.Space)) { - // There's no open menu, the first menu item should be highlight. - // The right way to do this is to SetFocus(MenuBar), but for some reason - // that faults. - - var mbar = GetMouseGrabViewInstance (this); - if (mbar != null) { - mbar.CleanUp (); - } - - //Activate (0); - //StartMenu (); - IsMenuOpen = true; - selected = 0; - CanFocus = true; - lastFocused = SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused; - SetFocus (); - SetNeedsDisplay (); - Application.GrabMouse (this); - } else if (!openedByHotKey) { - // There's an open menu. If this Alt key-up is a pre-cursor to an accelerator - // we don't want to close the menu because it'll flash. - // How to deal with that? - - CleanUp (); - } - - return true; - } - return false; - } - - internal void CleanUp () - { - isCleaning = true; - if (openMenu != null) { - CloseAllMenus (); - } - openedByAltKey = false; - IsMenuOpen = false; - selected = -1; - CanFocus = _initialCanFocus; - if (lastFocused != null) { - lastFocused.SetFocus (); - } - SetNeedsDisplay (); - Application.UngrabMouse (); - isCleaning = false; - } - - // The column where the MenuBar starts - static int xOrigin = 0; - // Spaces before the Title - static int leftPadding = 1; - // Spaces after the Title - static int rightPadding = 1; - // Spaces after the submenu Title, before Help - static int parensAroundHelp = 3; - /// - public override void OnDrawContent (Rect contentArea) - { - Move (0, 0); - Driver.SetAttribute (GetNormalColor ()); - for (int i = 0; i < Frame.Width; i++) - Driver.AddRune ((Rune)' '); - - Move (1, 0); - int pos = 0; - - for (int i = 0; i < Menus.Length; i++) { - var menu = Menus [i]; - Move (pos, 0); - Attribute hotColor, normalColor; - if (i == selected && IsMenuOpen) { - hotColor = i == selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; - normalColor = i == selected ? ColorScheme.Focus : GetNormalColor (); - } else { - hotColor = ColorScheme.HotNormal; - normalColor = GetNormalColor (); - } - // Note Help on MenuBar is drawn with parens around it - DrawHotString (string.IsNullOrEmpty (menu.Help) ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", hotColor, normalColor); - pos += leftPadding + menu.TitleLength + (menu.Help.GetColumns () > 0 ? leftPadding + menu.Help.GetColumns () + parensAroundHelp : 0) + rightPadding; - } - PositionCursor (); - } - - /// - public override void PositionCursor () - { - if (selected == -1 && HasFocus && Menus.Length > 0) { - selected = 0; - } - int pos = 0; - for (int i = 0; i < Menus.Length; i++) { - if (i == selected) { - pos++; - Move (pos + 1, 0); - return; - } else { - pos += leftPadding + Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + parensAroundHelp : 0) + rightPadding; - } - } - } - - void Selected (MenuItem item) - { - var action = item.Action; - - if (action == null) - return; - - Application.UngrabMouse (); - CloseAllMenus (); - Application.Refresh (); - - Run (action); - } - - internal void Run (Action action) - { - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } - - /// - /// Raised as a menu is opening. - /// - public event EventHandler MenuOpening; - - /// - /// Raised when a menu is opened. - /// - public event EventHandler MenuOpened; - - /// - /// Raised when a menu is closing passing . - /// - public event EventHandler MenuClosing; - - /// - /// Raised when all the menu is closed. - /// - public event EventHandler MenuAllClosed; - - // BUGBUG: Hack - internal Menu openMenu; - Menu ocm; - internal Menu openCurrentMenu { - get => ocm; - set { - if (ocm != value) { - ocm = value; - if (ocm != null && ocm.current > -1) { - OnMenuOpened (); - } - } - } - } - internal List openSubMenu; - View previousFocused; - internal bool isMenuOpening; - internal bool isMenuClosing; - - /// - /// if the menu is open; otherwise . - /// - public bool IsMenuOpen { get; protected set; } - - /// - /// Virtual method that will invoke the event if it's defined. - /// - /// The current menu to be replaced. - /// Returns the - public virtual MenuOpeningEventArgs OnMenuOpening (MenuBarItem currentMenu) - { - var ev = new MenuOpeningEventArgs (currentMenu); - MenuOpening?.Invoke (this, ev); - return ev; - } - - /// - /// Virtual method that will invoke the event if it's defined. - /// - public virtual void OnMenuOpened () - { - MenuItem mi = null; - MenuBarItem parent; - - if (openCurrentMenu.barItems.Children != null && openCurrentMenu.barItems.Children.Length > 0 - && openCurrentMenu?.current > -1) { - parent = openCurrentMenu.barItems; - mi = parent.Children [openCurrentMenu.current]; - } else if (openCurrentMenu.barItems.IsTopLevel) { - parent = null; - mi = openCurrentMenu.barItems; - } else { - parent = openMenu.barItems; - mi = parent.Children [openMenu.current]; - } - MenuOpened?.Invoke (this, new MenuOpenedEventArgs (parent, mi)); - } - - /// - /// Virtual method that will invoke the . - /// - /// The current menu to be closed. - /// Whether the current menu will be reopen. - /// Whether is a sub-menu or not. - public virtual MenuClosingEventArgs OnMenuClosing (MenuBarItem currentMenu, bool reopen, bool isSubMenu) - { - var ev = new MenuClosingEventArgs (currentMenu, reopen, isSubMenu); - MenuClosing?.Invoke (this, ev); - return ev; - } - - /// - /// Virtual method that will invoke the . - /// - public virtual void OnMenuAllClosed () - { - MenuAllClosed?.Invoke (this, EventArgs.Empty); - } - - View lastFocused; - - /// - /// Gets the view that was last focused before opening the menu. - /// - public View LastFocused { get; private set; } - - internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null) - { - isMenuOpening = true; - var newMenu = OnMenuOpening (Menus [index]); - if (newMenu.Cancel) { - isMenuOpening = false; - return; - } - if (newMenu.NewMenuBarItem != null) { - Menus [index] = newMenu.NewMenuBarItem; - } - int pos = 0; - switch (subMenu) { - case null: - // Open a submenu below a MenuBar - lastFocused ??= (SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused); - if (openSubMenu != null && !CloseMenu (false, true)) - return; - if (openMenu != null) { - Application.Current.Remove (openMenu); - openMenu.Dispose (); - openMenu = null; - } - - // This positions the submenu horizontally aligned with the first character of the - // text belonging to the menu - for (int i = 0; i < index; i++) - pos += Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + 2 : 0) + leftPadding + rightPadding; - - var locationOffset = Point.Empty; - // if SuperView is null then it's from a ContextMenu - if (SuperView == null) { - locationOffset = GetScreenOffset (); - } - if (SuperView != null && SuperView != Application.Current) { - locationOffset.X += SuperView.Border.Thickness.Left; - locationOffset.Y += SuperView.Border.Thickness.Top; - } - openMenu = new Menu (this, Frame.X + pos + locationOffset.X, Frame.Y + 1 + locationOffset.Y, Menus [index], null, MenusBorderStyle); - openCurrentMenu = openMenu; - openCurrentMenu.previousSubFocused = openMenu; - - Application.Current.Add (openMenu); - openMenu.SetFocus (); - break; - default: - // Opens a submenu next to another submenu (openSubMenu) - if (openSubMenu == null) - openSubMenu = new List (); - if (sIndex > -1) { - RemoveSubMenu (sIndex); - } else { - var last = openSubMenu.Count > 0 ? openSubMenu.Last () : openMenu; - if (!UseSubMenusSingleFrame) { - locationOffset = GetLocationOffset (); - openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width + locationOffset.X, last.Frame.Top + locationOffset.Y + last.current, subMenu, last, MenusBorderStyle); - } else { - var first = openSubMenu.Count > 0 ? openSubMenu.First () : openMenu; - // 2 is for the parent and the separator - var mbi = new MenuItem [2 + subMenu.Children.Length]; - mbi [0] = new MenuItem () { Title = subMenu.Title, Parent = subMenu }; - mbi [1] = null; - for (int j = 0; j < subMenu.Children.Length; j++) { - mbi [j + 2] = subMenu.Children [j]; - } - var newSubMenu = new MenuBarItem (mbi) { Parent = subMenu }; - openCurrentMenu = new Menu (this, first.Frame.Left, first.Frame.Top, newSubMenu, null, MenusBorderStyle); - last.Visible = false; - Application.GrabMouse (openCurrentMenu); - } - openCurrentMenu.previousSubFocused = last.previousSubFocused; - openSubMenu.Add (openCurrentMenu); - Application.Current.Add (openCurrentMenu); - } - selectedSub = openSubMenu.Count - 1; - if (selectedSub > -1 && SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current)) { - openCurrentMenu.SetFocus (); - } - break; - } - isMenuOpening = false; - IsMenuOpen = true; - } - - Point GetLocationOffset () - { - if (MenusBorderStyle != LineStyle.None) { - return new Point (0, 1); - } - return new Point (-2, 0); - } - - /// - /// Opens the Menu programatically, as though the F9 key were pressed. - /// - public void OpenMenu () - { - var mbar = GetMouseGrabViewInstance (this); - if (mbar != null) { - mbar.CleanUp (); - } - - if (openMenu != null) - return; - selected = 0; - SetNeedsDisplay (); - - previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused; - OpenMenu (selected); - if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) { - return; - } - if (!openCurrentMenu.CheckSubMenu ()) - return; - Application.GrabMouse (this); - } - - // Activates the menu, handles either first focus, or activating an entry when it was already active - // For mouse events. - internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null) - { - selected = idx; - selectedSub = sIdx; - if (openMenu == null) - previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused; - - OpenMenu (idx, sIdx, subMenu); - SetNeedsDisplay (); - } - - internal bool SelectEnabledItem (IEnumerable chldren, int current, out int newCurrent, bool forward = true) - { - if (chldren == null) { - newCurrent = -1; - return true; - } - - IEnumerable childrens; - if (forward) { - childrens = chldren; - } else { - childrens = chldren.Reverse (); - } - int count; - if (forward) { - count = -1; - } else { - count = childrens.Count (); - } - foreach (var child in childrens) { - if (forward) { - if (++count < current) { - continue; - } - } else { - if (--count > current) { - continue; - } - } - if (child == null || !child.IsEnabled ()) { - if (forward) { - current++; - } else { - current--; - } - } else { - newCurrent = current; - return true; - } - } - newCurrent = -1; - return false; - } - - /// - /// Closes the Menu programmatically if open and not canceled (as though F9 were pressed). - /// - public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) - { - return CloseMenu (false, false, ignoreUseSubMenusSingleFrame); - } - - bool reopen; - - internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) - { - var mbi = isSubMenu ? openCurrentMenu.barItems : openMenu?.barItems; - if (UseSubMenusSingleFrame && mbi != null && - !ignoreUseSubMenusSingleFrame && mbi.Parent != null) { - return false; - } - isMenuClosing = true; - this.reopen = reopen; - var args = OnMenuClosing (mbi, reopen, isSubMenu); - if (args.Cancel) { - isMenuClosing = false; - if (args.CurrentMenu.Parent != null) - openMenu.current = ((MenuBarItem)args.CurrentMenu.Parent).Children.IndexOf (args.CurrentMenu); - return false; - } - switch (isSubMenu) { - case false: - if (openMenu != null) { - Application.Current.Remove (openMenu); - } - SetNeedsDisplay (); - if (previousFocused != null && previousFocused is Menu && openMenu != null && previousFocused.ToString () != openCurrentMenu.ToString ()) - previousFocused.SetFocus (); - openMenu?.Dispose (); - openMenu = null; - if (lastFocused is Menu || lastFocused is MenuBar) { - lastFocused = null; - } - LastFocused = lastFocused; - lastFocused = null; - if (LastFocused != null && LastFocused.CanFocus) { - if (!reopen) { - selected = -1; - } - if (openSubMenu != null) { - openSubMenu = null; - } - if (openCurrentMenu != null) { - Application.Current.Remove (openCurrentMenu); - openCurrentMenu.Dispose (); - openCurrentMenu = null; - } - LastFocused.SetFocus (); - } else if (openSubMenu == null || openSubMenu.Count == 0) { - CloseAllMenus (); - } else { - SetFocus (); - PositionCursor (); - } - IsMenuOpen = false; - break; - - case true: - selectedSub = -1; - SetNeedsDisplay (); - RemoveAllOpensSubMenus (); - openCurrentMenu.previousSubFocused.SetFocus (); - openSubMenu = null; - IsMenuOpen = true; - break; - } - this.reopen = false; - isMenuClosing = false; - return true; - } - - void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false) - { - if (openSubMenu == null || (UseSubMenusSingleFrame - && !ignoreUseSubMenusSingleFrame && openSubMenu.Count == 0)) - - return; - for (int i = openSubMenu.Count - 1; i > index; i--) { - isMenuClosing = true; - Menu menu; - if (openSubMenu.Count - 1 > 0) - menu = openSubMenu [i - 1]; - else - menu = openMenu; - if (!menu.Visible) - menu.Visible = true; - openCurrentMenu = menu; - openCurrentMenu.SetFocus (); - if (openSubMenu != null) { - menu = openSubMenu [i]; - Application.Current.Remove (menu); - openSubMenu.Remove (menu); - menu.Dispose (); - } - RemoveSubMenu (i, ignoreUseSubMenusSingleFrame); - } - if (openSubMenu.Count > 0) - openCurrentMenu = openSubMenu.Last (); - - isMenuClosing = false; - } - - internal void RemoveAllOpensSubMenus () - { - if (openSubMenu != null) { - foreach (var item in openSubMenu) { - Application.Current.Remove (item); - item.Dispose (); - } - } - } - - internal void CloseAllMenus () - { - if (!isMenuOpening && !isMenuClosing) { - if (openSubMenu != null && !CloseMenu (false, true, true)) - return; - if (!CloseMenu (false)) - return; - if (LastFocused != null && LastFocused != this) - selected = -1; - Application.UngrabMouse (); - } - IsMenuOpen = false; - openedByHotKey = false; - openedByAltKey = false; - OnMenuAllClosed (); - } - - View FindDeepestMenu (View view, ref int count) - { - count = count > 0 ? count : 0; - foreach (var menu in view.Subviews) { - if (menu is Menu) { - count++; - return FindDeepestMenu ((Menu)menu, ref count); - } - } - return view; - } - - internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) - { - switch (isSubMenu) { - case false: - if (selected <= 0) - selected = Menus.Length - 1; - else - selected--; - - if (selected > -1 && !CloseMenu (true, false, ignoreUseSubMenusSingleFrame)) - return; - OpenMenu (selected); - if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current, false)) { - openCurrentMenu.current = 0; - } - break; - case true: - if (selectedSub > -1) { - selectedSub--; - RemoveSubMenu (selectedSub, ignoreUseSubMenusSingleFrame); - SetNeedsDisplay (); - } else - PreviousMenu (); - - break; - } - } - - internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) - { - switch (isSubMenu) { - case false: - if (selected == -1) - selected = 0; - else if (selected + 1 == Menus.Length) - selected = 0; - else - selected++; - - if (selected > -1 && !CloseMenu (true, ignoreUseSubMenusSingleFrame)) - return; - OpenMenu (selected); - SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current); - break; - case true: - if (UseKeysUpDownAsKeysLeftRight) { - if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) { - NextMenu (false, ignoreUseSubMenusSingleFrame); - } - } else { - var subMenu = openCurrentMenu.current > -1 && openCurrentMenu.barItems.Children.Length > 0 - ? openCurrentMenu.barItems.SubMenu (openCurrentMenu.barItems.Children [openCurrentMenu.current]) - : null; - if ((selectedSub == -1 || openSubMenu == null || openSubMenu?.Count - 1 == selectedSub) && subMenu == null) { - if (openSubMenu != null && !CloseMenu (false, true)) - return; - NextMenu (false, ignoreUseSubMenusSingleFrame); - } else if (subMenu != null || (openCurrentMenu.current > -1 - && !openCurrentMenu.barItems.Children [openCurrentMenu.current].IsFromSubMenu)) { - selectedSub++; - openCurrentMenu.CheckSubMenu (); - } else { - if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) { - NextMenu (false, ignoreUseSubMenusSingleFrame); - } - return; - } - - SetNeedsDisplay (); - if (UseKeysUpDownAsKeysLeftRight) - openCurrentMenu.CheckSubMenu (); - } - break; - } - } - - bool openedByHotKey; - internal bool FindAndOpenMenuByHotkey (KeyEvent kb) - { - //int pos = 0; - var c = ((uint)kb.Key & (uint)Key.CharMask); - for (int i = 0; i < Menus.Length; i++) { - // TODO: this code is duplicated, hotkey should be part of the MenuBarItem - var mi = Menus [i]; - int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier.ToString ()); - if (p != -1 && p + 1 < mi.Title.GetRuneCount ()) { - if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) { - ProcessMenu (i, mi); - return true; - } else if (mi.Children?.Length > 0) { - if (FindAndOpenChildrenMenuByHotkey (kb, mi.Children)) { - return true; - } - } - } else if (mi.Children?.Length > 0) { - if (FindAndOpenChildrenMenuByHotkey (kb, mi.Children)) { - return true; - } - } - } - - return false; - } - - bool FindAndOpenChildrenMenuByHotkey (KeyEvent kb, MenuItem [] children) - { - var c = ((uint)kb.Key & (uint)Key.CharMask); - for (int i = 0; i < children.Length; i++) { - var mi = children [i]; - - if(mi == null) { - continue; - } - - int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier.ToString ()); - if (p != -1 && p + 1 < mi.Title.GetRuneCount ()) { - if (Char.ToUpperInvariant ((char)mi.Title [p + 1]) == c) { - if (mi.IsEnabled ()) { - var action = mi.Action; - if (action != null) { - Run (action); - } - } - return true; - } else if (mi is MenuBarItem menuBarItem && menuBarItem?.Children.Length > 0) { - if (FindAndOpenChildrenMenuByHotkey (kb, menuBarItem.Children)) { - return true; - } - } - } else if (mi is MenuBarItem menuBarItem && menuBarItem?.Children.Length > 0) { - if (FindAndOpenChildrenMenuByHotkey (kb, menuBarItem.Children)) { - return true; - } - } - } - return false; - } - - internal bool FindAndOpenMenuByShortcut (KeyEvent kb, MenuItem [] children = null) - { - if (children == null) { - children = Menus; - } - - var key = kb.KeyValue; - var keys = ShortcutHelper.GetModifiersKey (kb); - key |= (int)keys; - for (int i = 0; i < children.Length; i++) { - var mi = children [i]; - if (mi == null) { - continue; - } - if ((!(mi is MenuBarItem mbiTopLevel) || mbiTopLevel.IsTopLevel) && mi.Shortcut != Key.Null && mi.Shortcut == (Key)key) { - var action = mi.Action; - if (action != null) { - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } - return true; - } - if (mi is MenuBarItem menuBarItem && menuBarItem.Children != null && !menuBarItem.IsTopLevel && FindAndOpenMenuByShortcut (kb, menuBarItem.Children)) { - return true; - } - } - - return false; - } - - private void ProcessMenu (int i, MenuBarItem mi) - { - if (selected < 0 && IsMenuOpen) { - return; - } - - if (mi.IsTopLevel) { - BoundsToScreen (i, 0, out int rx, out int ry); - var menu = new Menu (this, rx, ry, mi, null, MenusBorderStyle); - menu.Run (mi.Action); - menu.Dispose (); - } else { - openedByHotKey = true; - Application.GrabMouse (this); - selected = i; - OpenMenu (i); - if (!SelectEnabledItem (openCurrentMenu.barItems.Children, openCurrentMenu.current, out openCurrentMenu.current) && !CloseMenu (false)) { - return; - } - if (!openCurrentMenu.CheckSubMenu ()) - return; - } - SetNeedsDisplay (); - } - - /// - public override bool ProcessHotKey (KeyEvent kb) - { - if (kb.Key == Key) { - if (Visible && !IsMenuOpen) { - OpenMenu (); - } else { - CloseAllMenus (); - } - return true; - } - - // To ncurses simulate a AltMask key pressing Alt+Space because - // it can't detect an alone special key down was pressed. - if (kb.IsAlt && kb.Key == Key.AltMask && openMenu == null) { - OnKeyDown (kb); - OnKeyUp (kb); - return true; - } else if (kb.IsAlt && !kb.IsCtrl && !kb.IsShift) { - if (FindAndOpenMenuByHotkey (kb)) return true; - } - //var kc = kb.KeyValue; - - return base.ProcessHotKey (kb); - } - - /// - public override bool ProcessKey (KeyEvent kb) - { - if (InvokeKeybindings (kb) == true) - return true; - - var key = kb.KeyValue; - if ((key >= 'a' && key <= 'z') || (key >= 'A' && key <= 'Z') || (key >= '0' && key <= '9')) { - char c = Char.ToUpper ((char)key); - - if (selected == -1 || Menus [selected].IsTopLevel) - return false; - - foreach (var mi in Menus [selected].Children) { - if (mi == null) - continue; - int p = mi.Title.IndexOf (MenuBar.HotKeySpecifier.ToString ()); - if (p != -1 && p + 1 < mi.Title.GetRuneCount ()) { - if (mi.Title [p + 1] == c) { - Selected (mi); - return true; - } - } - } - } - - return false; - } - - void CloseMenuBar () - { - if (!CloseMenu (false)) - return; - if (openedByAltKey) { - openedByAltKey = false; - LastFocused?.SetFocus (); - } - SetNeedsDisplay (); - } - - void MoveRight () - { - selected = (selected + 1) % Menus.Length; - OpenMenu (selected); - SetNeedsDisplay (); - } - - void MoveLeft () - { - selected--; - if (selected < 0) - selected = Menus.Length - 1; - OpenMenu (selected); - SetNeedsDisplay (); - } - - /// - public override bool ProcessColdKey (KeyEvent kb) - { - return FindAndOpenMenuByShortcut (kb); - } - - /// - public override bool MouseEvent (MouseEvent me) - { - if (!handled && !HandleGrabView (me, this)) { - return false; - } - handled = false; - - if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked || - (me.Flags == MouseFlags.ReportMousePosition && selected > -1) || - (me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && selected > -1)) { - int pos = xOrigin; - Point locationOffset = default; - if (SuperView != null) { - locationOffset.X += SuperView.Border.Thickness.Left; - locationOffset.Y += SuperView.Border.Thickness.Top; - } - int cx = me.X - locationOffset.X; - for (int i = 0; i < Menus.Length; i++) { - if (cx >= pos && cx < pos + leftPadding + Menus [i].TitleLength + Menus [i].Help.GetColumns () + rightPadding) { - if (me.Flags == MouseFlags.Button1Clicked) { - if (Menus [i].IsTopLevel) { - BoundsToScreen (i, 0, out int rx, out int ry); - var menu = new Menu (this, rx, ry, Menus [i], null, MenusBorderStyle); - menu.Run (Menus [i].Action); - menu.Dispose (); - } else if (!IsMenuOpen) { - Activate (i); - } - } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked) { - if (IsMenuOpen && !Menus [i].IsTopLevel) { - CloseAllMenus (); - } else if (!Menus [i].IsTopLevel) { - Activate (i); - } - } else if (selected != i && selected > -1 && (me.Flags == MouseFlags.ReportMousePosition || - me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition)) { - if (IsMenuOpen) { - if (!CloseMenu (true, false)) { - return true; - } - Activate (i); - } - } else if (IsMenuOpen) { - if (!UseSubMenusSingleFrame || (UseSubMenusSingleFrame && openCurrentMenu != null - && openCurrentMenu.barItems.Parent != null && openCurrentMenu.barItems.Parent.Parent != Menus [i])) { - - Activate (i); - } - } - return true; - } else if (i == Menus.Length - 1 && me.Flags == MouseFlags.Button1Clicked) { - if (IsMenuOpen && !Menus [i].IsTopLevel) { - CloseAllMenus (); - return true; - } - } - pos += leftPadding + Menus [i].TitleLength + rightPadding; - } - } - return false; - } - - internal bool handled; - internal bool isContextMenuLoading; - - internal bool HandleGrabView (MouseEvent me, View current) - { - if (Application.MouseGrabView != null) { - if (me.View is MenuBar || me.View is Menu) { - var mbar = GetMouseGrabViewInstance (me.View); - if (mbar != null) { - if (me.Flags == MouseFlags.Button1Clicked) { - mbar.CleanUp (); - Application.GrabMouse (me.View); - } else { - handled = false; - return false; - } - } - if (me.View != current) { - Application.UngrabMouse (); - var v = me.View; - Application.GrabMouse (v); - MouseEvent nme; - if (me.Y > -1) { - var newxy = v.ScreenToFrame (me.X, me.Y); - nme = new MouseEvent () { - X = newxy.X, - Y = newxy.Y, - Flags = me.Flags, - OfX = me.X - newxy.X, - OfY = me.Y - newxy.Y, - View = v - }; - } else { - nme = new MouseEvent () { - X = me.X + current.Frame.X, - Y = 0, - Flags = me.Flags, - View = v - }; - } - - v.MouseEvent (nme); - return false; - } - } else if (!isContextMenuLoading && !(me.View is MenuBar || me.View is Menu) - && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { - - Application.UngrabMouse (); - if (IsMenuOpen) - CloseAllMenus (); - handled = false; - return false; - } else { - handled = false; - isContextMenuLoading = false; - return false; - } - } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked - || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { - - Application.GrabMouse (current); - } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { - Application.GrabMouse (me.View); - } else { - handled = false; - return false; - } - - handled = true; - - return true; - } - - MenuBar GetMouseGrabViewInstance (View view) - { - if (view == null || Application.MouseGrabView == null) { - return null; - } - - MenuBar hostView = null; - if (view is MenuBar) { - hostView = (MenuBar)view; - } else if (view is Menu) { - hostView = ((Menu)view).host; - } - - var grabView = Application.MouseGrabView; - MenuBar hostGrabView = null; - if (grabView is MenuBar) { - hostGrabView = (MenuBar)grabView; - } else if (grabView is Menu) { - hostGrabView = ((Menu)grabView).host; - } - - return hostView != hostGrabView ? hostGrabView : null; - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - - return base.OnEnter (view); - } - - /// - /// Gets the superview location offset relative to the location. - /// - /// The location offset. - internal Point GetScreenOffset () - { - var superViewFrame = SuperView == null ? new Rect (0, 0, Driver.Cols, Driver.Rows) : SuperView.Frame; - var sv = SuperView == null ? Application.Current : SuperView; - var boundsOffset = sv.GetBoundsOffset (); - return new Point (superViewFrame.X - sv.Frame.X - boundsOffset.X, - superViewFrame.Y - sv.Frame.Y - boundsOffset.Y); - } - - /// - /// Gets the location offset relative to the location. - /// - /// The location offset. - internal Point GetScreenOffsetFromCurrent () - { - var screen = new Rect (0, 0, Driver.Cols, Driver.Rows); - var currentFrame = Application.Current.Frame; - var boundsOffset = Application.Top.GetBoundsOffset (); - return new Point (screen.X - currentFrame.X - boundsOffset.X - , screen.Y - currentFrame.Y - boundsOffset.Y); - } - } -} diff --git a/Terminal.Gui/Views/Menu/ContextMenu.cs b/Terminal.Gui/Views/Menu/ContextMenu.cs new file mode 100644 index 0000000000..53983536b8 --- /dev/null +++ b/Terminal.Gui/Views/Menu/ContextMenu.cs @@ -0,0 +1,242 @@ +using System; + +namespace Terminal.Gui; + +/// +/// ContextMenu provides a pop-up menu that can be positioned anywhere within a . +/// ContextMenu is analogous to and, once activated, works like a sub-menu +/// of a (but can be positioned anywhere). +/// +/// By default, a ContextMenu with sub-menus is displayed in a cascading manner, where each sub-menu pops out of the ContextMenu frame +/// (either to the right or left, depending on where the ContextMenu is relative to the edge of the screen). By setting +/// to , this behavior can be changed such that all sub-menus are +/// drawn within the ContextMenu frame. +/// +/// +/// ContextMenus can be activated using the Shift-F10 key (by default; use the to change to another key). +/// +/// +/// Callers can cause the ContextMenu to be activated on a right-mouse click (or other interaction) by calling . +/// +/// +/// ContextMenus are located using screen using screen coordinates and appear above all other Views. +/// +/// +public sealed class ContextMenu : IDisposable { + /// + /// The default shortcut key for activating the context menu. + /// + [SerializableConfigurationProperty (Scope = typeof (SettingsScope))] + public static Key DefaultKey { get; set; } = Key.F10.WithShift; + + static MenuBar _menuBar; + Key _key = DefaultKey; + MouseFlags _mouseFlags = MouseFlags.Button3Clicked; + Toplevel _container; + + /// + /// Initializes a context menu with no menu items. + /// + public ContextMenu () : this (0, 0, new MenuBarItem ()) { } + + /// + /// Initializes a context menu, with a specifying the parent/host of the menu. + /// + /// The host view. + /// The menu items for the context menu. + public ContextMenu (View host, MenuBarItem menuItems) : + this (host.Frame.X, host.Frame.Y, menuItems) + { + Host = host; + } + + /// + /// Initializes a context menu with menu items at a specific screen location. + /// + /// The left position (screen relative). + /// The top position (screen relative). + /// The menu items. + public ContextMenu (int x, int y, MenuBarItem menuItems) + { + if (IsShow) { + if (_menuBar.SuperView != null) { + Hide (); + } + IsShow = false; + } + MenuItems = menuItems; + Position = new Point (x, y); + } + + void MenuBar_MenuAllClosed (object sender, EventArgs e) + { + Dispose (); + } + + /// + /// Disposes the context menu object. + /// + public void Dispose () + { + if (IsShow) { + _menuBar.MenuAllClosed -= MenuBar_MenuAllClosed; + _menuBar.Dispose (); + _menuBar = null; + IsShow = false; + } + if (_container != null) { + _container.Closing -= Container_Closing; + } + } + + /// + /// Shows (opens) the ContextMenu, displaying the s it contains. + /// + public void Show () + { + if (_menuBar != null) { + Hide (); + } + _container = Application.Current; + _container.Closing += Container_Closing; + var frame = new Rect (0, 0, View.Driver.Cols, View.Driver.Rows); + var position = Position; + if (Host != null) { + Host.BoundsToScreen (frame.X, frame.Y, out int x, out int y); + var pos = new Point (x, y); + pos.Y += Host.Frame.Height - 1; + if (position != pos) { + Position = position = pos; + } + } + var rect = Menu.MakeFrame (position.X, position.Y, MenuItems.Children); + if (rect.Right >= frame.Right) { + if (frame.Right - rect.Width >= 0 || !ForceMinimumPosToZero) { + position.X = frame.Right - rect.Width; + } else if (ForceMinimumPosToZero) { + position.X = 0; + } + } else if (ForceMinimumPosToZero && position.X < 0) { + position.X = 0; + } + if (rect.Bottom >= frame.Bottom) { + if (frame.Bottom - rect.Height - 1 >= 0 || !ForceMinimumPosToZero) { + if (Host == null) { + position.Y = frame.Bottom - rect.Height - 1; + } else { + Host.BoundsToScreen (frame.X, frame.Y, out int x, out int y); + var pos = new Point (x, y); + position.Y = pos.Y - rect.Height - 1; + } + } else if (ForceMinimumPosToZero) { + position.Y = 0; + } + } else if (ForceMinimumPosToZero && position.Y < 0) { + position.Y = 0; + } + + _menuBar = new MenuBar (new [] { MenuItems }) { + X = position.X, + Y = position.Y, + Width = 0, + Height = 0, + UseSubMenusSingleFrame = UseSubMenusSingleFrame, + Key = Key + }; + + _menuBar._isContextMenuLoading = true; + _menuBar.MenuAllClosed += MenuBar_MenuAllClosed; + _menuBar.BeginInit (); + _menuBar.EndInit (); + IsShow = true; + _menuBar.OpenMenu (); + } + + void Container_Closing (object sender, ToplevelClosingEventArgs obj) + { + Hide (); + } + + /// + /// Hides (closes) the ContextMenu. + /// + public void Hide () + { + _menuBar?.CleanUp (); + Dispose (); + } + + /// + /// Event invoked when the is changed. + /// + public event EventHandler KeyChanged; + + /// + /// Event invoked when the is changed. + /// + public event EventHandler MouseFlagsChanged; + + /// + /// Gets or sets the menu position. + /// + public Point Position { get; set; } + + /// + /// Gets or sets the menu items for this context menu. + /// + public MenuBarItem MenuItems { get; set; } + + /// + /// Specifies the key that will activate the context menu. + /// + public Key Key { + get => _key; + set { + var oldKey = _key; + _key = value; + KeyChanged?.Invoke (this, new KeyChangedEventArgs (oldKey, _key)); + } + } + + /// + /// specifies the mouse action used to activate the context menu by mouse. + /// + public MouseFlags MouseFlags { + get => _mouseFlags; + set { + var oldFlags = _mouseFlags; + _mouseFlags = value; + MouseFlagsChanged?.Invoke (this, new MouseFlagsChangedEventArgs (oldFlags, value)); + } + } + + /// + /// Gets whether the ContextMenu is showing or not. + /// + public static bool IsShow { get; private set; } + + /// + /// The host which position will be used, + /// otherwise if it's null the container will be used. + /// + public View Host { get; set; } + + /// + /// Sets or gets whether the context menu be forced to the right, ensuring it is not clipped, if the x position + /// is less than zero. The default is which means the context menu will be forced to the right. + /// If set to , the context menu will be clipped on the left if x is less than zero. + /// + public bool ForceMinimumPosToZero { get; set; } = true; + + /// + /// Gets the that is hosting this context menu. + /// + public MenuBar MenuBar => _menuBar; + + /// + /// Gets or sets if sub-menus will be displayed using a "single frame" menu style. If , the ContextMenu + /// and any sub-menus that would normally cascade will be displayed within a single frame. If (the default), + /// sub-menus will cascade using separate frames for each level of the menu hierarchy. + /// + public bool UseSubMenusSingleFrame { get; set; } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/Menu.cs b/Terminal.Gui/Views/Menu/Menu.cs new file mode 100644 index 0000000000..34739612b8 --- /dev/null +++ b/Terminal.Gui/Views/Menu/Menu.cs @@ -0,0 +1,1029 @@ +using System; +using System.Diagnostics; +using System.Text; +using System.Linq; +using System.Reflection.Metadata; + +namespace Terminal.Gui; + +/// +/// Specifies how a shows selection state. +/// +[Flags] +public enum MenuItemCheckStyle { + /// + /// The menu item will be shown normally, with no check indicator. The default. + /// + NoCheck = 0b_0000_0000, + + /// + /// The menu item will indicate checked/un-checked state (see ). + /// + Checked = 0b_0000_0001, + + /// + /// The menu item is part of a menu radio group (see ) and will indicate selected state. + /// + Radio = 0b_0000_0010 +}; + +/// +/// A has title, an associated help text, and an action to execute on activation. +/// MenuItems can also have a checked indicator (see ). +/// +public class MenuItem { + string _title; + ShortcutHelper _shortcutHelper; + bool _allowNullChecked; + MenuItemCheckStyle _checkType; + + internal int TitleLength => GetMenuBarItemLength (Title); + + /// + /// Gets or sets arbitrary data for the menu item. + /// + /// This property is not used internally. + public object Data { get; set; } + + // TODO: Update to use Key instead of KeyCode + /// + /// Initializes a new instance of + /// + public MenuItem (KeyCode shortcut = KeyCode.Null) : this ("", "", null, null, null, shortcut) { } + + // TODO: Update to use Key instead of KeyCode + /// + /// Initializes a new instance of . + /// + /// Title for the menu item. + /// Help text to display. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executed. + /// The of this menu item. + /// The keystroke combination. + public MenuItem (string title, string help, Action action, Func canExecute = null, MenuItem parent = null, KeyCode shortcut = KeyCode.Null) + { + Title = title ?? ""; + Help = help ?? ""; + Action = action; + CanExecute = canExecute; + Parent = parent; + _shortcutHelper = new ShortcutHelper (); + if (shortcut != KeyCode.Null) { + Shortcut = shortcut; + } + } + + #region Keyboard Handling + + // TODO: Update to use Key instead of Rune + /// + /// The HotKey is used to activate a with the keyboard. HotKeys are defined by prefixing the + /// of a MenuItem with an underscore ('_'). + /// + /// Pressing Alt-Hotkey for a (menu items on the menu bar) works even if the menu is not active). + /// Once a menu has focus and is active, pressing just the HotKey will activate the MenuItem. + /// + /// + /// For example for a MenuBar with a "_File" MenuBarItem that contains a "_New" MenuItem, Alt-F will open the File menu. + /// Pressing the N key will then activate the New MenuItem. + /// + /// + /// See also which enable global key-bindings to menu items. + /// + /// + public Rune HotKey { get; set; } + + void GetHotKey () + { + bool nextIsHot = false; + foreach (char x in _title) { + if (x == MenuBar.HotKeySpecifier.Value) { + nextIsHot = true; + } else { + if (nextIsHot) { + HotKey = (Rune)char.ToUpper (x); + break; + } + nextIsHot = false; + HotKey = default; + } + } + } + + + // TODO: Update to use Key instead of KeyCode + /// + /// Shortcut defines a key binding to the MenuItem that will invoke the MenuItem's action globally for the that is + /// the parent of the or this . + /// + /// The will be drawn on the MenuItem to the right of the and text. See . + /// + /// + public KeyCode Shortcut { + get => _shortcutHelper.Shortcut; + set { + + if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == KeyCode.Null)) { + _shortcutHelper.Shortcut = value; + } + } + } + + /// + /// Gets the text describing the keystroke combination defined by . + /// + public string ShortcutTag => Key.ToString (_shortcutHelper.Shortcut, MenuBar.ShortcutDelimiter); + #endregion Keyboard Handling + + /// + /// Gets or sets the title of the menu item . + /// + /// The title. + public string Title { + get => _title; + set { + if (_title != value) { + _title = value; + GetHotKey (); + } + } + } + + /// + /// Gets or sets the help text for the menu item. The help text is drawn to the right of the . + /// + /// The help text. + public string Help { get; set; } + + /// + /// Gets or sets the action to be invoked when the menu item is triggered. + /// + /// Method to invoke. + public Action Action { get; set; } + + /// + /// Gets or sets the action to be invoked to determine if the menu can be triggered. If returns + /// the menu 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; } + + /// + /// Returns if the menu item is enabled. This method is a wrapper around . + /// + public bool IsEnabled () + { + return CanExecute == null ? true : CanExecute (); + } + + // + // ┌─────────────────────────────┐ + // │ Quit Quit UI Catalog Ctrl+Q │ + // └─────────────────────────────┘ + // ┌─────────────────┐ + // │ ◌ TopLevel Alt+T │ + // └─────────────────┘ + // TODO: Replace the `2` literals with named constants + internal int Width => 1 + // space before Title + TitleLength + + 2 + // space after Title - BUGBUG: This should be 1 + (Checked == true || CheckType.HasFlag (MenuItemCheckStyle.Checked) || CheckType.HasFlag (MenuItemCheckStyle.Radio) ? 2 : 0) + // check glyph + space + (Help.GetColumns () > 0 ? 2 + Help.GetColumns () : 0) + // Two spaces before Help + (ShortcutTag.GetColumns () > 0 ? 2 + ShortcutTag.GetColumns () : 0); // Pad two spaces before shortcut tag (which are also aligned right) + + /// + /// Sets or gets whether the shows a check indicator or not. See . + /// + public bool? Checked { set; get; } + + /// + /// Used only if is of type. + /// If allows to be null, true or false. + /// If only allows to be true or false. + /// + public bool AllowNullChecked { + get => _allowNullChecked; + set { + _allowNullChecked = value; + if (Checked == null) { + Checked = false; + } + } + } + + /// + /// Sets or gets the of a menu item where is set to . + /// + public MenuItemCheckStyle CheckType { + get => _checkType; + set { + _checkType = value; + if (_checkType == MenuItemCheckStyle.Checked && !_allowNullChecked && Checked == null) { + Checked = false; + } + } + } + + /// + /// Gets the parent for this . + /// + /// The parent. + public MenuItem Parent { get; set; } + + /// + /// Gets if this is from a sub-menu. + /// + internal bool IsFromSubMenu => Parent != null; + + /// + /// Merely a debugging aid to see the interaction with main. + /// + public MenuItem GetMenuItem () + { + return this; + } + + /// + /// Merely a debugging aid to see the interaction with main. + /// + public bool GetMenuBarItem () + { + return IsFromSubMenu; + } + + /// + /// Toggle the between three states if is + /// or between two states if is . + /// + public void ToggleChecked () + { + if (_checkType != MenuItemCheckStyle.Checked) { + throw new InvalidOperationException ("This isn't a Checked MenuItemCheckStyle!"); + } + bool? previousChecked = Checked; + if (AllowNullChecked) { + switch (previousChecked) { + case null: + Checked = true; + break; + case true: + Checked = false; + break; + case false: + Checked = null; + break; + } + } else { + Checked = !Checked; + } + } + + + int GetMenuBarItemLength (string title) + { + int len = 0; + foreach (var ch in title.EnumerateRunes ()) { + if (ch == MenuBar.HotKeySpecifier) { + continue; + } + len += Math.Max (ch.GetColumns (), 1); + } + + return len; + } +} + +/// +/// An internal class used to represent a menu pop-up menu. Created and managed by and . +/// +class Menu : View { + internal MenuBarItem _barItems; + internal MenuBar _host; + internal int _currentChild; + internal View _previousSubFocused; + + internal static Rect MakeFrame (int x, int y, MenuItem [] items, Menu parent = null, LineStyle border = LineStyle.Single) + { + if (items == null || items.Length == 0) { + return new Rect (); + } + int minX = x; + int minY = y; + int borderOffset = 2; // This 2 is for the space around + int maxW = (items.Max (z => z?.Width) ?? 0) + borderOffset; + int maxH = items.Length + borderOffset; + if (parent != null && x + maxW > Driver.Cols) { + minX = Math.Max (parent.Frame.Right - parent.Frame.Width - maxW, 0); + } + if (y + maxH > Driver.Rows) { + minY = Math.Max (Driver.Rows - maxH, 0); + } + return new Rect (minX, minY, maxW, maxH); + } + + public Menu (MenuBar host, int x, int y, MenuBarItem barItems, Menu parent = null, LineStyle border = LineStyle.Single) + : base (MakeFrame (x, y, barItems?.Children, parent, border)) + { + if (host == null) { + throw new ArgumentNullException (nameof (host)); + } + + if (barItems == null) { + throw new ArgumentNullException (nameof (barItems)); + } + + _host = host; + _barItems = barItems; + + if (barItems is { IsTopLevel: true }) { + // This is a standalone MenuItem on a MenuBar + ColorScheme = host.ColorScheme; + CanFocus = true; + } else { + + _currentChild = -1; + for (int i = 0; i < barItems.Children?.Length; i++) { + if (barItems.Children [i]?.IsEnabled () == true) { + _currentChild = i; + break; + } + + } + ColorScheme = host.ColorScheme; + CanFocus = true; + WantMousePositionReports = host.WantMousePositionReports; + } + + BorderStyle = host.MenusBorderStyle; + + if (Application.Current != null) { + Application.Current.DrawContentComplete += Current_DrawContentComplete; + Application.Current.SizeChanging += Current_TerminalResized; + } + Application.MouseEvent += Application_RootMouseEvent; + + // Things this view knows how to do + AddCommand (Command.LineUp, () => MoveUp ()); + AddCommand (Command.LineDown, () => MoveDown ()); + AddCommand (Command.Left, () => { + _host.PreviousMenu (true); + return true; + }); + AddCommand (Command.Right, () => { + _host.NextMenu (!_barItems.IsTopLevel || _barItems.Children != null + && _barItems.Children.Length > 0 && _currentChild > -1 + && _currentChild < _barItems.Children.Length && _barItems.Children [_currentChild].IsFromSubMenu, + _barItems.Children != null && _barItems.Children.Length > 0 && _currentChild > -1 + && host.UseSubMenusSingleFrame && _barItems.SubMenu (_barItems.Children [_currentChild]) != null); + + return true; + }); + AddCommand (Command.Cancel, () => { + CloseAllMenus (); + return true; + }); + AddCommand (Command.Accept, () => { + RunSelected (); + return true; + }); + AddCommand (Command.Select, () => _host?.SelectItem (_menuItemToSelect)); + AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ()); + AddCommand (Command.Default, () => _host?.SelectItem (_menuItemToSelect)); + + // Default key bindings for this view + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.Esc, Command.Cancel); + KeyBindings.Add (KeyCode.Enter, Command.Accept); + KeyBindings.Add (KeyCode.F9, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + KeyBindings.Add (KeyCode.CtrlMask | KeyCode.Space, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + + AddKeyBindings (barItems); +#if SUPPORT_ALT_TO_ACTIVATE_MENU + Initialized += (s, e) => { + if (SuperView != null) { + SuperView.KeyUp += SuperView_KeyUp; + } + }; +#endif + // Debugging aid so ToString() is helpful + Text = _barItems.Title; + } + + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + void SuperView_KeyUp (object sender, KeyEventArgs e) + { + if (SuperView == null || SuperView.CanFocus == false || SuperView.Visible == false) { + return; + } + _host.AltKeyUpHandler (e); + } +#endif + + void AddKeyBindings (MenuBarItem menuBarItem) + { + if (menuBarItem == null || menuBarItem.Children == null) { + return; + } + foreach (var menuItem in menuBarItem.Children.Where (m => m != null)) { + KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse); + KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, Command.ToggleExpandCollapse); + if (menuItem.Shortcut != KeyCode.Unknown) { + KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select); + } + var subMenu = menuBarItem.SubMenu (menuItem); + AddKeyBindings (subMenu); + } + } + + int _menuBarItemToActivate = -1; + MenuItem _menuItemToSelect; + + /// + /// Called when a key bound to Command.Select is pressed. This means a hot key was pressed. + /// + /// + bool SelectOrRun () + { + if (!IsInitialized || !Visible) { + return true; + } + + if (_menuBarItemToActivate != -1) { + _host.Activate (1, _menuBarItemToActivate); + } else if (_menuItemToSelect != null) { + var m = _menuItemToSelect as MenuBarItem; + if (m?.Children?.Length > 0) { + + var item = _barItems.Children [_currentChild]; + if (item == null) { + return true; + } + bool disabled = item == null || !item.IsEnabled (); + if (!disabled && (_host.UseSubMenusSingleFrame || !CheckSubMenu ())) { + SetNeedsDisplay (); + SetParentSetNeedsDisplay (); + return true; + } + if (!disabled) { + _host.OnMenuOpened (); + } + + } else { + _host.SelectItem (_menuItemToSelect); + } + } else if (_host.IsMenuOpen) { + _host.CloseAllMenus (); + } else { + _host.OpenMenu (); + } + //_openedByHotKey = true; + return true; + } + + /// + public override bool? OnInvokingKeyBindings (Key keyEvent) + { + // This is a bit of a hack. We want to handle the key bindings for menu bar but + // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for. + // So before we call the base class we set SelectedItem appropriately. + + var key = keyEvent.KeyCode; + + if (KeyBindings.TryGet(key, out _)) { + _menuBarItemToActivate = -1; + _menuItemToSelect = null; + + var children = _barItems.Children; + if (children == null) { + return base.OnInvokingKeyBindings (keyEvent); + } + + // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item. + foreach (var c in children) { + if (key == c?.Shortcut) { + _menuBarItemToActivate = -1; + _menuItemToSelect = c; + keyEvent.Scope = KeyBindingScope.HotKey; + return base.OnInvokingKeyBindings (keyEvent); + } + var subMenu = _barItems.SubMenu (c); + if (FindShortcutInChildMenu (key, subMenu)) { + keyEvent.Scope = KeyBindingScope.HotKey; + return base.OnInvokingKeyBindings (keyEvent); + } + } + + // Search for hot keys next. + for (int c = 0; c < children.Length; c++) { + int hotKeyValue = children [c]?.HotKey.Value ?? default; + var hotKey = (KeyCode)hotKeyValue; + if (hotKey == KeyCode.Null) { + continue; + } + bool matches = key == hotKey || key == (hotKey | KeyCode.AltMask); + if (!_host.IsMenuOpen) { + // If the menu is open, only match if Alt is not pressed. + matches = key == hotKey; + } + + if (matches) { + _menuItemToSelect = children [c]; + _currentChild = c; + return base.OnInvokingKeyBindings (keyEvent); + } + } + } + + var handled = base.OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + return true; + } + + // This supports the case where the menu bar is a context menu + return _host.OnInvokingKeyBindings (keyEvent); + } + + bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem) + { + if (menuBarItem == null || menuBarItem.Children == null) { + return false; + } + foreach (var menuItem in menuBarItem.Children) { + if (key == menuItem?.Shortcut) { + _menuBarItemToActivate = -1; + _menuItemToSelect = menuItem; + return true; + } + var subMenu = menuBarItem.SubMenu (menuItem); + FindShortcutInChildMenu (key, subMenu); + } + return false; + } + + void Current_TerminalResized (object sender, SizeChangedEventArgs e) + { + if (_host.IsMenuOpen) { + _host.CloseAllMenus (); + } + } + + /// + public override void OnVisibleChanged () + { + base.OnVisibleChanged (); + if (Visible) { + Application.MouseEvent += Application_RootMouseEvent; + } else { + Application.MouseEvent -= Application_RootMouseEvent; + } + } + + void Application_RootMouseEvent (object sender, MouseEventEventArgs a) + { + if (a.MouseEvent.View is MenuBar) { + return; + } + var locationOffset = _host.GetScreenOffsetFromCurrent (); + if (SuperView != null && SuperView != Application.Current) { + locationOffset.X += SuperView.Border.Thickness.Left; + locationOffset.Y += SuperView.Border.Thickness.Top; + } + var view = FindDeepestView (this, a.MouseEvent.X + locationOffset.X, a.MouseEvent.Y + locationOffset.Y, out int rx, out int ry); + if (view == this) { + if (!Visible) { + throw new InvalidOperationException ("This shouldn't running on a invisible menu!"); + } + + var nme = new MouseEvent () { + X = rx, + Y = ry, + Flags = a.MouseEvent.Flags, + View = view + }; + if (MouseEvent (nme) || a.MouseEvent.Flags == MouseFlags.Button1Pressed || a.MouseEvent.Flags == MouseFlags.Button1Released) { + a.MouseEvent.Handled = true; + } + } + } + + internal Attribute DetermineColorSchemeFor (MenuItem item, int index) + { + if (item != null) { + if (index == _currentChild) { + return ColorScheme.Focus; + } + if (!item.IsEnabled ()) { + return ColorScheme.Disabled; + } + } + return GetNormalColor (); + } + + public override void OnDrawContent (Rect contentArea) + { + if (_barItems.Children == null) { + return; + } + var savedClip = Driver.Clip; + Driver.Clip = new Rect (0, 0, Driver.Cols, Driver.Rows); + Driver.SetAttribute (GetNormalColor ()); + + OnDrawFrames (); + OnRenderLineCanvas (); + + for (int i = Bounds.Y; i < _barItems.Children.Length; i++) { + if (i < 0) { + continue; + } + if (BoundsToScreen (Bounds).Y + i >= Driver.Rows) { + break; + } + var item = _barItems.Children [i]; + Driver.SetAttribute (item == null ? GetNormalColor () + : i == _currentChild ? ColorScheme.Focus : GetNormalColor ()); + if (item == null && BorderStyle != LineStyle.None) { + Move (-1, i); + Driver.AddRune (Glyphs.LeftTee); + } else if (Frame.X < Driver.Cols) { + Move (0, i); + } + + Driver.SetAttribute (DetermineColorSchemeFor (item, i)); + for (int p = Bounds.X; p < Frame.Width - 2; p++) { + // This - 2 is for the border + if (p < 0) { + continue; + } + if (BoundsToScreen (Bounds).X + p >= Driver.Cols) { + break; + } + if (item == null) { + Driver.AddRune (Glyphs.HLine); + } else if (i == 0 && p == 0 && _host.UseSubMenusSingleFrame && item.Parent.Parent != null) { + Driver.AddRune (Glyphs.LeftArrow); + } + // This `- 3` is left border + right border + one row in from right + else if (p == Frame.Width - 3 && _barItems.SubMenu (_barItems.Children [i]) != null) { + Driver.AddRune (Glyphs.RightArrow); + } else { + Driver.AddRune ((Rune)' '); + } + } + + if (item == null) { + if (BorderStyle != LineStyle.None && SuperView?.Frame.Right - Frame.X > Frame.Width) { + Move (Frame.Width - 2, i); + Driver.AddRune (Glyphs.RightTee); + } + continue; + } + + string textToDraw = null; + var nullCheckedChar = Glyphs.NullChecked; + var checkChar = Glyphs.Selected; + var uncheckedChar = Glyphs.UnSelected; + + if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked)) { + checkChar = Glyphs.Checked; + uncheckedChar = Glyphs.UnChecked; + } + + // Support Checked even though CheckType wasn't set + if (item.CheckType == MenuItemCheckStyle.Checked && item.Checked == null) { + textToDraw = $"{nullCheckedChar} {item.Title}"; + } else if (item.Checked == true) { + textToDraw = $"{checkChar} {item.Title}"; + } else if (item.CheckType.HasFlag (MenuItemCheckStyle.Checked) || item.CheckType.HasFlag (MenuItemCheckStyle.Radio)) { + textToDraw = $"{uncheckedChar} {item.Title}"; + } else { + textToDraw = item.Title; + } + + BoundsToScreen (0, i, out int vtsCol, out int vtsRow, false); + if (vtsCol < Driver.Cols) { + Driver.Move (vtsCol + 1, vtsRow); + if (!item.IsEnabled ()) { + DrawHotString (textToDraw, ColorScheme.Disabled, ColorScheme.Disabled); + } else if (i == 0 && _host.UseSubMenusSingleFrame && item.Parent.Parent != null) { + var tf = new TextFormatter () { + Alignment = TextAlignment.Centered, + HotKeySpecifier = MenuBar.HotKeySpecifier, + Text = textToDraw + }; + // The -3 is left/right border + one space (not sure what for) + tf.Draw (BoundsToScreen (new Rect (1, i, Frame.Width - 3, 1)), + i == _currentChild ? ColorScheme.Focus : GetNormalColor (), + i == _currentChild ? ColorScheme.HotFocus : ColorScheme.HotNormal, + SuperView == null ? default : SuperView.BoundsToScreen (SuperView.Bounds)); + } else { + DrawHotString (textToDraw, + i == _currentChild ? ColorScheme.HotFocus : ColorScheme.HotNormal, + i == _currentChild ? ColorScheme.Focus : GetNormalColor ()); + } + + // The help string + int l = item.ShortcutTag.GetColumns () == 0 ? item.Help.GetColumns () : item.Help.GetColumns () + item.ShortcutTag.GetColumns () + 2; + int col = Frame.Width - l - 3; + BoundsToScreen (col, i, out vtsCol, out vtsRow, false); + if (vtsCol < Driver.Cols) { + Driver.Move (vtsCol, vtsRow); + Driver.AddStr (item.Help); + + // The shortcut tag string + if (!string.IsNullOrEmpty (item.ShortcutTag)) { + Driver.Move (vtsCol + l - item.ShortcutTag.GetColumns (), vtsRow); + Driver.AddStr (item.ShortcutTag); + } + } + } + } + Driver.Clip = savedClip; + + PositionCursor (); + } + + void Current_DrawContentComplete (object sender, DrawEventArgs e) + { + if (Visible) { + OnDrawContent (Bounds); + } + } + + public override void PositionCursor () + { + if (_host == null || _host.IsMenuOpen) { + if (_barItems.IsTopLevel) { + _host.PositionCursor (); + } else { + Move (2, 1 + _currentChild); + } + } else { + _host.PositionCursor (); + } + } + + public void Run (Action action) + { + if (action == null || _host == null) { + return; + } + + Application.UngrabMouse (); + _host.CloseAllMenus (); + Application.Refresh (); + + _host.Run (action); + } + + public override bool OnLeave (View view) + { + return _host.OnLeave (view); + } + + void RunSelected () + { + if (_barItems.IsTopLevel) { + Run (_barItems.Action); + } else if (_currentChild > -1 && _barItems.Children [_currentChild].Action != null) { + Run (_barItems.Children [_currentChild].Action); + } else if (_currentChild == 0 && _host.UseSubMenusSingleFrame && _barItems.Children [_currentChild].Parent.Parent != null) { + _host.PreviousMenu (_barItems.Children [_currentChild].Parent.IsFromSubMenu, true); + } else if (_currentChild > -1 && _barItems.SubMenu (_barItems.Children [_currentChild]) != null) { + CheckSubMenu (); + } + } + + void CloseAllMenus () + { + Application.UngrabMouse (); + _host.CloseAllMenus (); + } + + bool MoveDown () + { + if (_barItems.IsTopLevel) { + return true; + } + bool disabled; + do { + _currentChild++; + if (_currentChild >= _barItems.Children.Length) { + _currentChild = 0; + } + if (this != _host.openCurrentMenu && _barItems.Children [_currentChild]?.IsFromSubMenu == true && _host._selectedSub > -1) { + _host.PreviousMenu (true); + _host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild); + _host.openCurrentMenu = this; + } + var item = _barItems.Children [_currentChild]; + if (item?.IsEnabled () != true) { + disabled = true; + } else { + disabled = false; + } + if (!_host.UseSubMenusSingleFrame && _host.UseKeysUpDownAsKeysLeftRight && _barItems.SubMenu (_barItems.Children [_currentChild]) != null && + !disabled && _host.IsMenuOpen) { + if (!CheckSubMenu ()) { + return false; + } + break; + } + if (!_host.IsMenuOpen) { + _host.OpenMenu (_host._selected); + } + } while (_barItems.Children [_currentChild] == null || disabled); + SetNeedsDisplay (); + SetParentSetNeedsDisplay (); + if (!_host.UseSubMenusSingleFrame) { + _host.OnMenuOpened (); + } + return true; + } + + bool MoveUp () + { + if (_barItems.IsTopLevel || _currentChild == -1) { + return true; + } + bool disabled; + do { + _currentChild--; + if (_host.UseKeysUpDownAsKeysLeftRight && !_host.UseSubMenusSingleFrame) { + if ((_currentChild == -1 || this != _host.openCurrentMenu) && _barItems.Children [_currentChild + 1].IsFromSubMenu && _host._selectedSub > -1) { + _currentChild++; + _host.PreviousMenu (true); + if (_currentChild > 0) { + _currentChild--; + _host.openCurrentMenu = this; + } + break; + } + } + if (_currentChild < 0) { + _currentChild = _barItems.Children.Length - 1; + } + if (!_host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild, false)) { + _currentChild = 0; + if (!_host.SelectEnabledItem (_barItems.Children, _currentChild, out _currentChild) && !_host.CloseMenu (false)) { + return false; + } + break; + } + var item = _barItems.Children [_currentChild]; + if (item?.IsEnabled () != true) { + disabled = true; + } else { + disabled = false; + } + if (!_host.UseSubMenusSingleFrame && _host.UseKeysUpDownAsKeysLeftRight && + _barItems.SubMenu (_barItems.Children [_currentChild]) != null && + !disabled && _host.IsMenuOpen) { + if (!CheckSubMenu ()) { + return false; + } + break; + } + } while (_barItems.Children [_currentChild] == null || disabled); + SetNeedsDisplay (); + SetParentSetNeedsDisplay (); + if (!_host.UseSubMenusSingleFrame) { + _host.OnMenuOpened (); + } + return true; + } + + void SetParentSetNeedsDisplay () + { + if (_host._openSubMenu != null) { + foreach (var menu in _host._openSubMenu) { + menu.SetNeedsDisplay (); + } + } + + _host?._openMenu?.SetNeedsDisplay (); + _host.SetNeedsDisplay (); + } + + public override bool MouseEvent (MouseEvent me) + { + if (!_host._handled && !_host.HandleGrabView (me, this)) { + return false; + } + _host._handled = false; + bool disabled; + int meY = me.Y - (Border == null ? 0 : Border.Thickness.Top); + if (me.Flags == MouseFlags.Button1Clicked) { + disabled = false; + if (meY < 0) { + return true; + } + if (meY >= _barItems.Children.Length) { + return true; + } + var item = _barItems.Children [meY]; + if (item == null || !item.IsEnabled ()) { + disabled = true; + } + if (disabled) { + return true; + } + _currentChild = meY; + if (item != null && !disabled) { + RunSelected (); + } + return true; + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || + me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.ReportMousePosition || + me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition)) { + + disabled = false; + if (meY < 0 || meY >= _barItems.Children.Length) { + return true; + } + var item = _barItems.Children [meY]; + if (item == null) { + return true; + } + if (item == null || !item.IsEnabled ()) { + disabled = true; + } + if (item != null && !disabled) { + _currentChild = meY; + } + if (_host.UseSubMenusSingleFrame || !CheckSubMenu ()) { + SetNeedsDisplay (); + SetParentSetNeedsDisplay (); + return true; + } + _host.OnMenuOpened (); + return true; + } + return false; + } + + internal bool CheckSubMenu () + { + if (_currentChild == -1 || _barItems.Children [_currentChild] == null) { + return true; + } + var subMenu = _barItems.SubMenu (_barItems.Children [_currentChild]); + if (subMenu != null) { + int pos = -1; + if (_host._openSubMenu != null) { + pos = _host._openSubMenu.FindIndex (o => o?._barItems == subMenu); + } + if (pos == -1 && this != _host.openCurrentMenu && subMenu.Children != _host.openCurrentMenu._barItems.Children + && !_host.CloseMenu (false, true)) { + return false; + } + _host.Activate (_host._selected, pos, subMenu); + } else if (_host._openSubMenu?.Count == 0 || _host._openSubMenu?.Last ()._barItems.IsSubMenuOf (_barItems.Children [_currentChild]) == false) { + return _host.CloseMenu (false, true); + } else { + SetNeedsDisplay (); + SetParentSetNeedsDisplay (); + } + return true; + } + + int GetSubMenuIndex (MenuBarItem subMenu) + { + int pos = -1; + if (this != null && Subviews.Count > 0) { + Menu v = null; + foreach (var menu in Subviews) { + if (((Menu)menu)._barItems == subMenu) { + v = (Menu)menu; + } + } + if (v != null) { + pos = Subviews.IndexOf (v); + } + } + + return pos; + } + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + + return base.OnEnter (view); + } + + protected override void Dispose (bool disposing) + { + if (Application.Current != null) { + Application.Current.DrawContentComplete -= Current_DrawContentComplete; + Application.Current.SizeChanging -= Current_TerminalResized; + } + Application.MouseEvent -= Application_RootMouseEvent; + base.Dispose (disposing); + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/MenuBar.cs b/Terminal.Gui/Views/Menu/MenuBar.cs new file mode 100644 index 0000000000..b749348120 --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuBar.cs @@ -0,0 +1,1467 @@ +using System; +using System.Text; +using System.Linq; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Terminal.Gui; + +/// +/// is a menu item on . +/// MenuBarItems do not support . +/// +public class MenuBarItem : MenuItem { + /// + /// Initializes a new as a . + /// + /// Title for the menu item. + /// Help text to display. Will be displayed next to the Title surrounded by parentheses. + /// Action to invoke when the menu item is activated. + /// Function to determine if the action can currently be executed. + /// The parent of this if exist, otherwise is null. + public MenuBarItem (string title, string help, Action action, Func canExecute = null, MenuItem parent = null) : base (title, help, action, canExecute, parent) + { + Initialize (title, null, null, true); + } + + /// + /// Initializes a new . + /// + /// Title for the menu item. + /// The items in the current menu. + /// The parent of this if exist, otherwise is null. + public MenuBarItem (string title, MenuItem [] children, MenuItem parent = null) + { + Initialize (title, children, parent); + } + + /// + /// Initializes a new with separate list of items. + /// + /// Title for the menu item. + /// The list of items in the current menu. + /// The parent of this if exist, otherwise is null. + public MenuBarItem (string title, List children, MenuItem parent = null) + { + Initialize (title, children, parent); + } + + /// + /// Initializes a new . + /// + /// The items in the current menu. + public MenuBarItem (MenuItem [] children) : this ("", children) { } + + /// + /// Initializes a new . + /// + public MenuBarItem () : this (children: new MenuItem [] { }) { } + + void Initialize (string title, object children, MenuItem parent = null, bool isTopLevel = false) + { + if (!isTopLevel && children == null) { + throw new ArgumentNullException (nameof (children), "The parameter cannot be null. Use an empty array instead."); + } + SetTitle (title ?? ""); + if (parent != null) { + Parent = parent; + } + if (children is List childrenList) { + var newChildren = new MenuItem [] { }; + foreach (var grandChild in childrenList) { + foreach (var child in grandChild) { + SetParent (grandChild); + Array.Resize (ref newChildren, newChildren.Length + 1); + newChildren [newChildren.Length - 1] = child; + } + + } + Children = newChildren; + } else if (children is MenuItem [] items) { + SetParent (items); + Children = items; + } else { + Children = null; + } + } + + void SetParent (MenuItem [] children) + { + foreach (var child in children) { + if (child is { Parent: null }) { + child.Parent = this; + } + } + } + + /// + /// Check if a is a . + /// + /// + /// Returns a or null otherwise. + public MenuBarItem SubMenu (MenuItem menuItem) + { + return menuItem as MenuBarItem; + } + + /// + /// Check if a is a submenu of this MenuBar. + /// + /// + /// Returns true if it is a submenu. false otherwise. + public bool IsSubMenuOf (MenuItem menuItem) + { + foreach (var child in Children) { + if (child == menuItem && child.Parent == menuItem.Parent) { + return true; + } + } + return false; + } + + /// + /// Get the index of a child . + /// + /// + /// Returns a greater than -1 if the is a child. + public int GetChildrenIndex (MenuItem children) + { + int i = 0; + if (Children != null) { + foreach (var child in Children) { + if (child == children) { + return i; + } + i++; + } + } + return -1; + } + + void SetTitle (string title) + { + title ??= string.Empty; + Title = title; + } + + /// + /// Gets or sets an array of objects that are the children of this + /// + /// The children. + public MenuItem [] Children { get; set; } + + internal bool IsTopLevel => Parent == null && (Children == null || Children.Length == 0) && Action != null; + + internal void AddKeyBindings (MenuBar menuBar) + { + if (Children == null) { + return; + } + foreach (var menuItem in Children.Where (m => m != null)) { + if (menuItem.HotKey != default) { + menuBar.KeyBindings.Add ((KeyCode)menuItem.HotKey.Value, Command.ToggleExpandCollapse); + menuBar.KeyBindings.Add ((KeyCode)menuItem.HotKey.Value | KeyCode.AltMask, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + } + if (menuItem.Shortcut != KeyCode.Unknown && menuItem.Shortcut != KeyCode.Null) { + menuBar.KeyBindings.Add (menuItem.Shortcut, KeyBindingScope.HotKey, Command.Select); + } + SubMenu (menuItem)?.AddKeyBindings (menuBar); + } + } +} + +/// +/// +/// Provides a menu bar that spans the top of a View with drop-down and cascading menus. +/// +/// +/// By default, any sub-sub-menus (sub-menus of the s added to s) +/// are displayed in a cascading manner, where each sub-sub-menu pops out of the sub-menu frame +/// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting +/// to , this behavior can be changed such that all sub-sub-menus are +/// drawn within a single frame below the MenuBar. +/// +/// +/// +/// +/// The appears on the first row of the SuperView and uses the full width. +/// +/// +/// See also: +/// +/// +/// The provides global hot keys for the application. See . +/// +/// +/// When the menu is created key bindings for each menu item and its sub-menu items are added for each menu item's +/// hot key (both alone AND with AltMask) and shortcut, if defined. +/// +/// +/// If a key press matches any of the menu item's hot keys or shortcuts, the menu item's action is invoked or +/// sub-menu opened. +/// +/// +/// * If the menu bar is not open +/// * Any shortcut defined within the menu will be invoked +/// * Only hot keys defined for the menu bar items will be invoked, and only if Alt is pressed too. +/// * If the menu bar is open +/// * Un-shifted hot keys defined for the menu bar items will be invoked, only if the menu they belong to is open (the menu bar item's text is visible). +/// * Alt-shifted hot keys defined for the menu bar items will be invoked, only if the menu they belong to is open (the menu bar item's text is visible). +/// * If there is a visible hot key that duplicates a shortcut (e.g. _File and Alt-F), the hot key wins. +/// +/// +public class MenuBar : View { + internal int _selected; + internal int _selectedSub; + + /// + /// Gets or sets the array of s for the menu. Only set this after the is visible. + /// + /// The menu array. + public MenuBarItem [] Menus { get; set; } + + /// + /// The default for 's border. The default is . + /// + public LineStyle MenusBorderStyle { get; set; } = LineStyle.Single; + + bool _useSubMenusSingleFrame; + + /// + /// Gets or sets if the sub-menus must be displayed in a single or multiple frames. + /// + /// By default any sub-sub-menus (sub-menus of the main s) are displayed in a cascading manner, + /// where each sub-sub-menu pops out of the sub-menu frame + /// (either to the right or left, depending on where the sub-menu is relative to the edge of the screen). By setting + /// to , this behavior can be changed such that all sub-sub-menus are + /// drawn within a single frame below the MenuBar. + /// + /// + public bool UseSubMenusSingleFrame { + get => _useSubMenusSingleFrame; + set { + _useSubMenusSingleFrame = value; + if (value && UseKeysUpDownAsKeysLeftRight) { + _useKeysUpDownAsKeysLeftRight = false; + SetNeedsDisplay (); + } + } + } + + /// + public override bool Visible { + get => base.Visible; + set { + base.Visible = value; + if (!value) { + CloseAllMenus (); + } + } + } + + /// + /// Initializes a new instance of the . + /// + public MenuBar () : this (new MenuBarItem [] { }) { } + + /// + /// Initializes a new instance of the class with the specified set of Toplevel menu items. + /// + /// Individual menu items; a null item will result in a separator being drawn. + public MenuBar (MenuBarItem [] menus) : base () + { + X = 0; + Y = 0; + Width = Dim.Fill (); + Height = 1; + Menus = menus; + //CanFocus = true; + _selected = -1; + _selectedSub = -1; + ColorScheme = Colors.Menu; + WantMousePositionReports = true; + IsMenuOpen = false; + + Added += MenuBar_Added; + + // Things this view knows how to do + AddCommand (Command.Left, () => { + MoveLeft (); + return true; + }); + AddCommand (Command.Right, () => { + MoveRight (); + return true; + }); + AddCommand (Command.Cancel, () => { + CloseMenuBar (); + return true; + }); + AddCommand (Command.Accept, () => { + ProcessMenu (_selected, Menus [_selected]); + return true; + }); + + AddCommand (Command.ToggleExpandCollapse, () => SelectOrRun ()); + AddCommand (Command.Select, () => Run (_menuItemToSelect?.Action)); + + // Default key bindings for this view + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.Esc, Command.Cancel); + KeyBindings.Add (KeyCode.CursorDown, Command.Accept); + KeyBindings.Add (KeyCode.Enter, Command.Accept); + KeyBindings.Add ((KeyCode)Key, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + KeyBindings.Add (KeyCode.CtrlMask | KeyCode.Space, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + + // TODO: Bindings (esp for hotkey) should be added across and then down. This currently does down then across. + // TODO: As a result, _File._Save will have precedence over in "_File _Edit _ScrollbarView" + // TODO: Also: Hotkeys should not work for sub-menus if they are not visible! + if (Menus != null) { + foreach (var menuBarItem in Menus?.Where (m => m != null)) { + if (menuBarItem.HotKey != default) { + KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value, Command.ToggleExpandCollapse); + KeyBindings.Add ((KeyCode)menuBarItem.HotKey.Value | KeyCode.AltMask, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + } + if (menuBarItem.Shortcut != KeyCode.Unknown && menuBarItem.Shortcut != KeyCode.Null) { + // Technically this will will never run because MenuBarItems don't have shortcuts + KeyBindings.Add (menuBarItem.Shortcut, KeyBindingScope.HotKey, Command.Select); + } + menuBarItem.AddKeyBindings (this); + } + } + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + // Enable the Alt key as a menu activator + Initialized += (s, e) => { + if (SuperView != null) { + SuperView.KeyUp += SuperView_KeyUp; + } + }; +#endif + } + +#if SUPPORT_ALT_TO_ACTIVATE_MENU + void SuperView_KeyUp (object sender, KeyEventArgs e) + { + if (SuperView == null || SuperView.CanFocus == false || SuperView.Visible == false) { + return; + } + AltKeyUpHandler(e); + } +#endif + + internal void AltKeyUpHandler (Key e) + { + if (e.KeyCode == KeyCode.AltMask) { + e.Handled = true; + // User pressed Alt + if (!IsMenuOpen && _openMenu == null && !_openedByAltKey) { + // There's no open menu, the first menu item should be highlighted. + // The right way to do this is to SetFocus(MenuBar), but for some reason + // that faults. + + GetMouseGrabViewInstance (this)?.CleanUp (); + + IsMenuOpen = true; + _openedByAltKey = true; + _selected = 0; + CanFocus = true; + _lastFocused = SuperView == null ? Application.Current.MostFocused : SuperView.MostFocused; + SetFocus (); + SetNeedsDisplay (); + Application.GrabMouse (this); + } else if (!_openedByHotKey) { + // There's an open menu. Close it. + CleanUp (); + } else { + _openedByAltKey = false; + _openedByHotKey = false; + } + } + } + + #region Keyboard handling + Key _key = Key.F9; + + /// + /// The used to activate or close the menu bar by keyboard. The default is . + /// + /// + /// + /// If the user presses any s defined in the s, the menu bar will be activated and the sub-menu will be opened. + /// + /// + /// will close the menu bar and any open sub-menus. + /// + /// + public Key Key { + get => _key; + set { + if (_key == value) { + return; + } + KeyBindings.Remove (_key); + KeyBindings.Add (value, KeyBindingScope.HotKey, Command.ToggleExpandCollapse); + _key = value; + } + } + + + bool _useKeysUpDownAsKeysLeftRight = false; + + /// + /// Used for change the navigation key style. + /// + public bool UseKeysUpDownAsKeysLeftRight { + get => _useKeysUpDownAsKeysLeftRight; + set { + _useKeysUpDownAsKeysLeftRight = value; + if (value && UseSubMenusSingleFrame) { + UseSubMenusSingleFrame = false; + SetNeedsDisplay (); + } + } + } + + static Rune _shortcutDelimiter = new Rune ('+'); + + /// + /// Sets or gets the shortcut delimiter separator. The default is "+". + /// + public static Rune ShortcutDelimiter { + get => _shortcutDelimiter; + set { + if (_shortcutDelimiter != value) { + _shortcutDelimiter = value == default ? new Rune ('+') : value; + } + } + } + + /// + /// The specifier character for the hot keys. + /// + public new static Rune HotKeySpecifier => (Rune)'_'; + + // Set in OnInvokingKeyBindings. -1 means no menu item is selected for activation. + int _menuBarItemToActivate; + + // Set in OnInvokingKeyBindings. null means no sub-menu is selected for activation. + MenuItem _menuItemToSelect; + bool _openedByAltKey; + bool _openedByHotKey; + + /// + /// Called when a key bound to Command.Select is pressed. Either activates the menu item or runs it, depending on whether it has a sub-menu. + /// If the menu is open, it will close the menu bar. + /// + /// + bool SelectOrRun () + { + if (!IsInitialized || !Visible) { + return true; + } + + _openedByHotKey = true; + if (_menuBarItemToActivate != -1) { + Activate (_menuBarItemToActivate); + } else if (_menuItemToSelect != null) { + Run (_menuItemToSelect.Action); + } else { + if (IsMenuOpen && _openMenu != null) { + CloseAllMenus (); + } else { + OpenMenu (); + } + + } + return true; + } + + /// + public override bool? OnInvokingKeyBindings (Key keyEvent) + { + // This is a bit of a hack. We want to handle the key bindings for menu bar but + // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for. + // So before we call the base class we set SelectedItem appropriately. + // TODO: Figure out if there's a way to have KeyBindings pass context instead. Maybe a KeyBindingContext property? + + var key = keyEvent.KeyCode; + + if (KeyBindings.TryGet (key, out _)) { + _menuBarItemToActivate = -1; + _menuItemToSelect = null; + + // Search for shortcuts first. If there's a shortcut, we don't want to activate the menu item. + for (int i = 0; i < Menus.Length; i++) { + // Recurse through the menu to find one with the shortcut. + if (FindShortcutInChildMenu (key, Menus [i], out _menuItemToSelect)) { + _menuBarItemToActivate = i; + keyEvent.Scope = KeyBindingScope.HotKey; + return base.OnInvokingKeyBindings (keyEvent); + } + + // Now see if any of the menu bar items have a hot key that matches + // Technically this is not possible because menu bar items don't have + // shortcuts or Actions. But it's here for completeness. + var shortcut = Menus [i]?.Shortcut; + if (key == shortcut) { + throw new InvalidOperationException ("Menu bar items cannot have shortcuts"); + } + + } + + // Search for hot keys next. + for (int i = 0; i < Menus.Length; i++) { + if (IsMenuOpen) { + // We don't need to do anything because `Menu` will handle the key binding. + //break; + } + + // No submenu item matched (or the menu is closed) + + // Check if one of the menu bar item has a hot key that matches + int hotKeyValue = Menus [i]?.HotKey.Value ?? default; + var hotKey = (KeyCode)hotKeyValue; + if (hotKey != KeyCode.Null) { + bool matches = key == hotKey || key == (hotKey | KeyCode.AltMask); + if (IsMenuOpen) { + // If the menu is open, only match if Alt is not pressed. + matches = key == hotKey; + } + + if (matches) { + _menuBarItemToActivate = i; + keyEvent.Scope = KeyBindingScope.HotKey; + break; + } + } + + } + } + return base.OnInvokingKeyBindings (keyEvent); + } + + // TODO: Update to use Key instead of KeyCode + // Recurse the child menus looking for a shortcut that matches the key + bool FindShortcutInChildMenu (KeyCode key, MenuBarItem menuBarItem, out MenuItem menuItemToSelect) + { + menuItemToSelect = null; + + if (key == KeyCode.Null || menuBarItem?.Children == null) { + return false; + } + + for (int c = 0; c < menuBarItem.Children.Length; c++) { + var menuItem = menuBarItem.Children [c]; + if (key == menuItem?.Shortcut) { + menuItemToSelect = menuItem; + return true; + } + var subMenu = menuBarItem.SubMenu (menuItem); + if (subMenu != null) { + if (FindShortcutInChildMenu (key, subMenu, out menuItemToSelect)) { + return true; + } + } + } + return false; + } +#endregion Keyboard handling + + bool _initialCanFocus; + + void MenuBar_Added (object sender, SuperViewChangedEventArgs e) + { + _initialCanFocus = CanFocus; + Added -= MenuBar_Added; + } + + bool _isCleaning; + + internal void CleanUp () + { + _isCleaning = true; + if (_openMenu != null) { + CloseAllMenus (); + } + _openedByAltKey = false; + _openedByHotKey = false; + IsMenuOpen = false; + _selected = -1; + CanFocus = _initialCanFocus; + if (_lastFocused != null) { + _lastFocused.SetFocus (); + } + SetNeedsDisplay (); + Application.UngrabMouse (); + _isCleaning = false; + } + + // The column where the MenuBar starts + static int _xOrigin = 0; + + // Spaces before the Title + static int _leftPadding = 1; + + // Spaces after the Title + static int _rightPadding = 1; + + // Spaces after the submenu Title, before Help + static int _parensAroundHelp = 3; + + /// + public override void OnDrawContent (Rect contentArea) + { + Move (0, 0); + Driver.SetAttribute (GetNormalColor ()); + for (int i = 0; i < Frame.Width; i++) { + Driver.AddRune ((Rune)' '); + } + + Move (1, 0); + int pos = 0; + + for (int i = 0; i < Menus.Length; i++) { + var menu = Menus [i]; + Move (pos, 0); + Attribute hotColor, normalColor; + if (i == _selected && IsMenuOpen) { + hotColor = i == _selected ? ColorScheme.HotFocus : ColorScheme.HotNormal; + normalColor = i == _selected ? ColorScheme.Focus : GetNormalColor (); + } else { + hotColor = ColorScheme.HotNormal; + normalColor = GetNormalColor (); + } + // Note Help on MenuBar is drawn with parens around it + DrawHotString (string.IsNullOrEmpty (menu.Help) ? $" {menu.Title} " : $" {menu.Title} ({menu.Help}) ", hotColor, normalColor); + pos += _leftPadding + menu.TitleLength + (menu.Help.GetColumns () > 0 ? _leftPadding + menu.Help.GetColumns () + _parensAroundHelp : 0) + _rightPadding; + } + PositionCursor (); + } + + /// + public override void PositionCursor () + { + if (_selected == -1 && HasFocus && Menus.Length > 0) { + _selected = 0; + } + int pos = 0; + for (int i = 0; i < Menus.Length; i++) { + if (i == _selected) { + pos++; + Move (pos + 1, 0); + return; + } else { + pos += _leftPadding + Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + _parensAroundHelp : 0) + _rightPadding; + } + } + } + + /// + /// Called when an item is selected; Runs the action. + /// + /// + internal bool SelectItem (MenuItem item) + { + if (item?.Action == null) { + return false; + } + + Application.UngrabMouse (); + CloseAllMenus (); + Application.Refresh (); + _openedByAltKey = true; + return Run (item?.Action); + } + + internal bool Run (Action action) + { + if (action == null) { + return false; + } + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + return true; + } + + /// + /// Raised as a menu is opening. + /// + public event EventHandler MenuOpening; + + /// + /// Raised when a menu is opened. + /// + public event EventHandler MenuOpened; + + /// + /// Raised when a menu is closing passing . + /// + public event EventHandler MenuClosing; + + /// + /// Raised when all the menu is closed. + /// + public event EventHandler MenuAllClosed; + + // BUGBUG: Hack + internal Menu _openMenu; + Menu _ocm; + + internal Menu openCurrentMenu { + get => _ocm; + set { + if (_ocm != value) { + _ocm = value; + if (_ocm != null && _ocm._currentChild > -1) { + OnMenuOpened (); + } + } + } + } + + internal List _openSubMenu; + View _previousFocused; + internal bool _isMenuOpening; + internal bool _isMenuClosing; + + /// + /// if the menu is open; otherwise . + /// + public bool IsMenuOpen { get; protected set; } + + /// + /// Virtual method that will invoke the event if it's defined. + /// + /// The current menu to be replaced. + /// Returns the + public virtual MenuOpeningEventArgs OnMenuOpening (MenuBarItem currentMenu) + { + var ev = new MenuOpeningEventArgs (currentMenu); + MenuOpening?.Invoke (this, ev); + return ev; + } + + /// + /// Virtual method that will invoke the event if it's defined. + /// + public virtual void OnMenuOpened () + { + MenuItem mi = null; + MenuBarItem parent; + + if (openCurrentMenu._barItems.Children != null && openCurrentMenu._barItems.Children.Length > 0 + && openCurrentMenu?._currentChild > -1) { + parent = openCurrentMenu._barItems; + mi = parent.Children [openCurrentMenu._currentChild]; + } else if (openCurrentMenu._barItems.IsTopLevel) { + parent = null; + mi = openCurrentMenu._barItems; + } else { + parent = _openMenu._barItems; + mi = parent.Children [_openMenu._currentChild]; + } + MenuOpened?.Invoke (this, new MenuOpenedEventArgs (parent, mi)); + } + + /// + /// Virtual method that will invoke the . + /// + /// The current menu to be closed. + /// Whether the current menu will be reopen. + /// Whether is a sub-menu or not. + public virtual MenuClosingEventArgs OnMenuClosing (MenuBarItem currentMenu, bool reopen, bool isSubMenu) + { + var ev = new MenuClosingEventArgs (currentMenu, reopen, isSubMenu); + MenuClosing?.Invoke (this, ev); + return ev; + } + + /// + /// Virtual method that will invoke the . + /// + public virtual void OnMenuAllClosed () + { + MenuAllClosed?.Invoke (this, EventArgs.Empty); + } + + View _lastFocused; + + /// + /// Gets the view that was last focused before opening the menu. + /// + public View LastFocused { get; private set; } + + internal void OpenMenu (int index, int sIndex = -1, MenuBarItem subMenu = null) + { + _isMenuOpening = true; + var newMenu = OnMenuOpening (Menus [index]); + if (newMenu.Cancel) { + _isMenuOpening = false; + return; + } + if (newMenu.NewMenuBarItem != null) { + Menus [index] = newMenu.NewMenuBarItem; + } + int pos = 0; + switch (subMenu) { + case null: + // Open a submenu below a MenuBar + _lastFocused ??= SuperView == null ? Application.Current?.MostFocused : SuperView.MostFocused; + if (_openSubMenu != null && !CloseMenu (false, true)) { + return; + } + if (_openMenu != null) { + Application.Current.Remove (_openMenu); + _openMenu.Dispose (); + _openMenu = null; + } + + // This positions the submenu horizontally aligned with the first character of the + // text belonging to the menu + for (int i = 0; i < index; i++) { + pos += Menus [i].TitleLength + (Menus [i].Help.GetColumns () > 0 ? Menus [i].Help.GetColumns () + 2 : 0) + _leftPadding + _rightPadding; + } + + var locationOffset = Point.Empty; + // if SuperView is null then it's from a ContextMenu + if (SuperView == null) { + locationOffset = GetScreenOffset (); + } + if (SuperView != null && SuperView != Application.Current) { + locationOffset.X += SuperView.Border.Thickness.Left; + locationOffset.Y += SuperView.Border.Thickness.Top; + } + _openMenu = new Menu (this, Frame.X + pos + locationOffset.X, Frame.Y + 1 + locationOffset.Y, Menus [index], null, MenusBorderStyle); + openCurrentMenu = _openMenu; + openCurrentMenu._previousSubFocused = _openMenu; + + Application.Current.Add (_openMenu); + _openMenu.SetFocus (); + break; + default: + // Opens a submenu next to another submenu (openSubMenu) + if (_openSubMenu == null) { + _openSubMenu = new List (); + } + if (sIndex > -1) { + RemoveSubMenu (sIndex); + } else { + var last = _openSubMenu.Count > 0 ? _openSubMenu.Last () : _openMenu; + if (!UseSubMenusSingleFrame) { + locationOffset = GetLocationOffset (); + openCurrentMenu = new Menu (this, last.Frame.Left + last.Frame.Width + locationOffset.X, last.Frame.Top + locationOffset.Y + last._currentChild, subMenu, last, MenusBorderStyle); + } else { + var first = _openSubMenu.Count > 0 ? _openSubMenu.First () : _openMenu; + // 2 is for the parent and the separator + var mbi = new MenuItem [2 + subMenu.Children.Length]; + mbi [0] = new MenuItem () { Title = subMenu.Title, Parent = subMenu }; + mbi [1] = null; + for (int j = 0; j < subMenu.Children.Length; j++) { + mbi [j + 2] = subMenu.Children [j]; + } + var newSubMenu = new MenuBarItem (mbi) { Parent = subMenu }; + openCurrentMenu = new Menu (this, first.Frame.Left, first.Frame.Top, newSubMenu, null, MenusBorderStyle); + last.Visible = false; + Application.GrabMouse (openCurrentMenu); + } + openCurrentMenu._previousSubFocused = last._previousSubFocused; + _openSubMenu.Add (openCurrentMenu); + Application.Current.Add (openCurrentMenu); + } + _selectedSub = _openSubMenu.Count - 1; + if (_selectedSub > -1 && SelectEnabledItem (openCurrentMenu._barItems.Children, openCurrentMenu._currentChild, out openCurrentMenu._currentChild)) { + openCurrentMenu.SetFocus (); + } + break; + } + _isMenuOpening = false; + IsMenuOpen = true; + } + + Point GetLocationOffset () + { + if (MenusBorderStyle != LineStyle.None) { + return new Point (0, 1); + } + return new Point (-2, 0); + } + + /// + /// Opens the Menu programatically, as though the F9 key were pressed. + /// + public void OpenMenu () + { + var mbar = GetMouseGrabViewInstance (this); + if (mbar != null) { + mbar.CleanUp (); + } + + if (_openMenu != null) { + return; + } + _selected = 0; + SetNeedsDisplay (); + + _previousFocused = SuperView == null ? Application.Current.Focused : SuperView.Focused; + OpenMenu (_selected); + if (!SelectEnabledItem (openCurrentMenu._barItems.Children, openCurrentMenu._currentChild, out openCurrentMenu._currentChild) && !CloseMenu (false)) { + return; + } + if (!openCurrentMenu.CheckSubMenu ()) { + return; + } + Application.GrabMouse (this); + } + + // Activates the menu, handles either first focus, or activating an entry when it was already active + // For mouse events. + internal void Activate (int idx, int sIdx = -1, MenuBarItem subMenu = null) + { + _selected = idx; + _selectedSub = sIdx; + if (_openMenu == null) { + _previousFocused = SuperView == null ? Application.Current?.Focused ?? null : SuperView.Focused; + } + + OpenMenu (idx, sIdx, subMenu); + SetNeedsDisplay (); + } + + internal bool SelectEnabledItem (IEnumerable chldren, int current, out int newCurrent, bool forward = true) + { + if (chldren == null) { + newCurrent = -1; + return true; + } + + IEnumerable childrens; + if (forward) { + childrens = chldren; + } else { + childrens = chldren.Reverse (); + } + int count; + if (forward) { + count = -1; + } else { + count = childrens.Count (); + } + foreach (var child in childrens) { + if (forward) { + if (++count < current) { + continue; + } + } else { + if (--count > current) { + continue; + } + } + if (child == null || !child.IsEnabled ()) { + if (forward) { + current++; + } else { + current--; + } + } else { + newCurrent = current; + return true; + } + } + newCurrent = -1; + return false; + } + + /// + /// Closes the Menu programmatically if open and not canceled (as though F9 were pressed). + /// + public bool CloseMenu (bool ignoreUseSubMenusSingleFrame = false) + { + return CloseMenu (false, false, ignoreUseSubMenusSingleFrame); + } + + bool _reopen; + + internal bool CloseMenu (bool reopen = false, bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) + { + var mbi = isSubMenu ? openCurrentMenu._barItems : _openMenu?._barItems; + if (UseSubMenusSingleFrame && mbi != null && + !ignoreUseSubMenusSingleFrame && mbi.Parent != null) { + return false; + } + _isMenuClosing = true; + _reopen = reopen; + var args = OnMenuClosing (mbi, reopen, isSubMenu); + if (args.Cancel) { + _isMenuClosing = false; + if (args.CurrentMenu.Parent != null) { + _openMenu._currentChild = ((MenuBarItem)args.CurrentMenu.Parent).Children.IndexOf (args.CurrentMenu); + } + return false; + } + switch (isSubMenu) { + case false: + if (_openMenu != null) { + Application.Current.Remove (_openMenu); + } + SetNeedsDisplay (); + if (_previousFocused != null && _previousFocused is Menu && _openMenu != null && _previousFocused.ToString () != openCurrentMenu.ToString ()) { + _previousFocused.SetFocus (); + } + _openMenu?.Dispose (); + _openMenu = null; + if (_lastFocused is Menu || _lastFocused is MenuBar) { + _lastFocused = null; + } + LastFocused = _lastFocused; + _lastFocused = null; + if (LastFocused != null && LastFocused.CanFocus) { + if (!reopen) { + _selected = -1; + } + if (_openSubMenu != null) { + _openSubMenu = null; + } + if (openCurrentMenu != null) { + Application.Current.Remove (openCurrentMenu); + openCurrentMenu.Dispose (); + openCurrentMenu = null; + } + LastFocused.SetFocus (); + } else if (_openSubMenu == null || _openSubMenu.Count == 0) { + CloseAllMenus (); + } else { + SetFocus (); + PositionCursor (); + } + IsMenuOpen = false; + break; + + case true: + _selectedSub = -1; + SetNeedsDisplay (); + RemoveAllOpensSubMenus (); + openCurrentMenu._previousSubFocused.SetFocus (); + _openSubMenu = null; + IsMenuOpen = true; + break; + } + _reopen = false; + _isMenuClosing = false; + return true; + } + + void RemoveSubMenu (int index, bool ignoreUseSubMenusSingleFrame = false) + { + if (_openSubMenu == null || UseSubMenusSingleFrame + && !ignoreUseSubMenusSingleFrame && _openSubMenu.Count == 0) { + return; + } + for (int i = _openSubMenu.Count - 1; i > index; i--) { + _isMenuClosing = true; + Menu menu; + if (_openSubMenu.Count - 1 > 0) { + menu = _openSubMenu [i - 1]; + } else { + menu = _openMenu; + } + if (!menu.Visible) { + menu.Visible = true; + } + openCurrentMenu = menu; + openCurrentMenu.SetFocus (); + if (_openSubMenu != null) { + menu = _openSubMenu [i]; + Application.Current.Remove (menu); + _openSubMenu.Remove (menu); + menu.Dispose (); + } + RemoveSubMenu (i, ignoreUseSubMenusSingleFrame); + } + if (_openSubMenu.Count > 0) { + openCurrentMenu = _openSubMenu.Last (); + } + + _isMenuClosing = false; + } + + internal void RemoveAllOpensSubMenus () + { + if (_openSubMenu != null) { + foreach (var item in _openSubMenu) { + Application.Current.Remove (item); + item.Dispose (); + } + } + } + + internal void CloseAllMenus () + { + if (!_isMenuOpening && !_isMenuClosing) { + if (_openSubMenu != null && !CloseMenu (false, true, true)) { + return; + } + if (!CloseMenu (false)) { + return; + } + if (LastFocused != null && LastFocused != this) { + _selected = -1; + } + Application.UngrabMouse (); + } + IsMenuOpen = false; + _openedByAltKey = false; + _openedByHotKey = false; + OnMenuAllClosed (); + } + + internal void PreviousMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) + { + switch (isSubMenu) { + case false: + if (_selected <= 0) { + _selected = Menus.Length - 1; + } else { + _selected--; + } + + if (_selected > -1 && !CloseMenu (true, false, ignoreUseSubMenusSingleFrame)) { + return; + } + OpenMenu (_selected); + if (!SelectEnabledItem (openCurrentMenu._barItems.Children, openCurrentMenu._currentChild, out openCurrentMenu._currentChild, false)) { + openCurrentMenu._currentChild = 0; + } + break; + case true: + if (_selectedSub > -1) { + _selectedSub--; + RemoveSubMenu (_selectedSub, ignoreUseSubMenusSingleFrame); + SetNeedsDisplay (); + } else { + PreviousMenu (); + } + + break; + } + } + + internal void NextMenu (bool isSubMenu = false, bool ignoreUseSubMenusSingleFrame = false) + { + switch (isSubMenu) { + case false: + if (_selected == -1) { + _selected = 0; + } else if (_selected + 1 == Menus.Length) { + _selected = 0; + } else { + _selected++; + } + + if (_selected > -1 && !CloseMenu (true, ignoreUseSubMenusSingleFrame)) { + return; + } + OpenMenu (_selected); + SelectEnabledItem (openCurrentMenu._barItems.Children, openCurrentMenu._currentChild, out openCurrentMenu._currentChild); + break; + case true: + if (UseKeysUpDownAsKeysLeftRight) { + if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) { + NextMenu (false, ignoreUseSubMenusSingleFrame); + } + } else { + var subMenu = openCurrentMenu._currentChild > -1 && openCurrentMenu._barItems.Children.Length > 0 + ? openCurrentMenu._barItems.SubMenu (openCurrentMenu._barItems.Children [openCurrentMenu._currentChild]) + : null; + if ((_selectedSub == -1 || _openSubMenu == null || _openSubMenu?.Count - 1 == _selectedSub) && subMenu == null) { + if (_openSubMenu != null && !CloseMenu (false, true)) { + return; + } + NextMenu (false, ignoreUseSubMenusSingleFrame); + } else if (subMenu != null || openCurrentMenu._currentChild > -1 + && !openCurrentMenu._barItems.Children [openCurrentMenu._currentChild].IsFromSubMenu) { + _selectedSub++; + openCurrentMenu.CheckSubMenu (); + } else { + if (CloseMenu (false, true, ignoreUseSubMenusSingleFrame)) { + NextMenu (false, ignoreUseSubMenusSingleFrame); + } + return; + } + + SetNeedsDisplay (); + if (UseKeysUpDownAsKeysLeftRight) { + openCurrentMenu.CheckSubMenu (); + } + } + break; + } + } + + void ProcessMenu (int i, MenuBarItem mi) + { + if (_selected < 0 && IsMenuOpen) { + return; + } + + if (mi.IsTopLevel) { + BoundsToScreen (i, 0, out int rx, out int ry); + var menu = new Menu (this, rx, ry, mi, null, MenusBorderStyle); + menu.Run (mi.Action); + menu.Dispose (); + } else { + Application.GrabMouse (this); + _selected = i; + OpenMenu (i); + if (!SelectEnabledItem (openCurrentMenu._barItems.Children, openCurrentMenu._currentChild, out openCurrentMenu._currentChild) && !CloseMenu (false)) { + return; + } + if (!openCurrentMenu.CheckSubMenu ()) { + return; + } + } + SetNeedsDisplay (); + } + + + void CloseMenuBar () + { + if (!CloseMenu (false)) { + return; + } + if (_openedByAltKey) { + _openedByAltKey = false; + LastFocused?.SetFocus (); + } + SetNeedsDisplay (); + } + + void MoveRight () + { + _selected = (_selected + 1) % Menus.Length; + OpenMenu (_selected); + SetNeedsDisplay (); + } + + void MoveLeft () + { + _selected--; + if (_selected < 0) { + _selected = Menus.Length - 1; + } + OpenMenu (_selected); + SetNeedsDisplay (); + } + + #region Mouse Handling + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + + return base.OnEnter (view); + } + + /// + public override bool OnLeave (View view) + { + if ((!(view is MenuBar) && !(view is Menu) || !(view is MenuBar) && !(view is Menu) && _openMenu != null) && !_isCleaning && !_reopen) { + CleanUp (); + } + return base.OnLeave (view); + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!_handled && !HandleGrabView (me, this)) { + return false; + } + _handled = false; + + if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked || me.Flags == MouseFlags.Button1Clicked || + me.Flags == MouseFlags.ReportMousePosition && _selected > -1 || + me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition) && _selected > -1) { + int pos = _xOrigin; + Point locationOffset = default; + if (SuperView != null) { + locationOffset.X += SuperView.Border.Thickness.Left; + locationOffset.Y += SuperView.Border.Thickness.Top; + } + int cx = me.X - locationOffset.X; + for (int i = 0; i < Menus.Length; i++) { + if (cx >= pos && cx < pos + _leftPadding + Menus [i].TitleLength + Menus [i].Help.GetColumns () + _rightPadding) { + if (me.Flags == MouseFlags.Button1Clicked) { + if (Menus [i].IsTopLevel) { + BoundsToScreen (i, 0, out int rx, out int ry); + var menu = new Menu (this, rx, ry, Menus [i], null, MenusBorderStyle); + menu.Run (Menus [i].Action); + menu.Dispose (); + } else if (!IsMenuOpen) { + Activate (i); + } + } else if (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked || me.Flags == MouseFlags.Button1TripleClicked) { + if (IsMenuOpen && !Menus [i].IsTopLevel) { + CloseAllMenus (); + } else if (!Menus [i].IsTopLevel) { + Activate (i); + } + } else if (_selected != i && _selected > -1 && (me.Flags == MouseFlags.ReportMousePosition || + me.Flags == MouseFlags.Button1Pressed && me.Flags == MouseFlags.ReportMousePosition)) { + if (IsMenuOpen) { + if (!CloseMenu (true, false)) { + return true; + } + Activate (i); + } + } else if (IsMenuOpen) { + if (!UseSubMenusSingleFrame || UseSubMenusSingleFrame && openCurrentMenu != null + && openCurrentMenu._barItems.Parent != null && openCurrentMenu._barItems.Parent.Parent != Menus [i]) { + + Activate (i); + } + } + return true; + } else if (i == Menus.Length - 1 && me.Flags == MouseFlags.Button1Clicked) { + if (IsMenuOpen && !Menus [i].IsTopLevel) { + CloseAllMenus (); + return true; + } + } + pos += _leftPadding + Menus [i].TitleLength + _rightPadding; + } + } + return false; + } + + internal bool _handled; + internal bool _isContextMenuLoading; + + internal bool HandleGrabView (MouseEvent me, View current) + { + if (Application.MouseGrabView != null) { + if (me.View is MenuBar || me.View is Menu) { + var mbar = GetMouseGrabViewInstance (me.View); + if (mbar != null) { + if (me.Flags == MouseFlags.Button1Clicked) { + mbar.CleanUp (); + Application.GrabMouse (me.View); + } else { + _handled = false; + return false; + } + } + if (me.View != current) { + Application.UngrabMouse (); + var v = me.View; + Application.GrabMouse (v); + MouseEvent nme; + if (me.Y > -1) { + var newxy = v.ScreenToFrame (me.X, me.Y); + nme = new MouseEvent () { + X = newxy.X, + Y = newxy.Y, + Flags = me.Flags, + OfX = me.X - newxy.X, + OfY = me.Y - newxy.Y, + View = v + }; + } else { + nme = new MouseEvent () { + X = me.X + current.Frame.X, + Y = 0, + Flags = me.Flags, + View = v + }; + } + + v.MouseEvent (nme); + return false; + } + } else if (!_isContextMenuLoading && !(me.View is MenuBar || me.View is Menu) + && me.Flags != MouseFlags.ReportMousePosition && me.Flags != 0) { + + Application.UngrabMouse (); + if (IsMenuOpen) { + CloseAllMenus (); + } + _handled = false; + return false; + } else { + _handled = false; + _isContextMenuLoading = false; + return false; + } + } else if (!IsMenuOpen && (me.Flags == MouseFlags.Button1Pressed || me.Flags == MouseFlags.Button1DoubleClicked + || me.Flags == MouseFlags.Button1TripleClicked || me.Flags.HasFlag (MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition))) { + + Application.GrabMouse (current); + } else if (IsMenuOpen && (me.View is MenuBar || me.View is Menu)) { + Application.GrabMouse (me.View); + } else { + _handled = false; + return false; + } + + _handled = true; + + return true; + } + + MenuBar GetMouseGrabViewInstance (View view) + { + if (view == null || Application.MouseGrabView == null) { + return null; + } + + MenuBar hostView = null; + if (view is MenuBar) { + hostView = (MenuBar)view; + } else if (view is Menu) { + hostView = ((Menu)view)._host; + } + + var grabView = Application.MouseGrabView; + MenuBar hostGrabView = null; + if (grabView is MenuBar) { + hostGrabView = (MenuBar)grabView; + } else if (grabView is Menu) { + hostGrabView = ((Menu)grabView)._host; + } + + return hostView != hostGrabView ? hostGrabView : null; + } + #endregion Mouse Handling + + /// + /// Gets the superview location offset relative to the location. + /// + /// The location offset. + internal Point GetScreenOffset () + { + if (Driver == null) { + return Point.Empty; + } + var superViewFrame = SuperView == null ? new Rect (0, 0, Driver.Cols, Driver.Rows) : SuperView.Frame; + var sv = SuperView == null ? Application.Current : SuperView; + var boundsOffset = sv.GetBoundsOffset (); + return new Point (superViewFrame.X - sv.Frame.X - boundsOffset.X, + superViewFrame.Y - sv.Frame.Y - boundsOffset.Y); + } + + /// + /// Gets the location offset relative to the location. + /// + /// The location offset. + internal Point GetScreenOffsetFromCurrent () + { + var screen = new Rect (0, 0, Driver.Cols, Driver.Rows); + var currentFrame = Application.Current.Frame; + var boundsOffset = Application.Top.GetBoundsOffset (); + return new Point (screen.X - currentFrame.X - boundsOffset.X + , screen.Y - currentFrame.Y - boundsOffset.Y); + } +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Menu/MenuEventArgs.cs b/Terminal.Gui/Views/Menu/MenuEventArgs.cs new file mode 100644 index 0000000000..906c6050a8 --- /dev/null +++ b/Terminal.Gui/Views/Menu/MenuEventArgs.cs @@ -0,0 +1,97 @@ +using System; + +namespace Terminal.Gui; +/// +/// An which allows passing a cancelable menu opening event or replacing with a new . +/// +public class MenuOpeningEventArgs : EventArgs { + /// + /// The current parent. + /// + public MenuBarItem CurrentMenu { get; } + + /// + /// The new to be replaced. + /// + public MenuBarItem NewMenuBarItem { get; set; } + /// + /// Flag that allows the cancellation of the event. If set to in the + /// event handler, the event will be canceled. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of . + /// + /// The current parent. + public MenuOpeningEventArgs (MenuBarItem currentMenu) + { + CurrentMenu = currentMenu; + } +} + +/// +/// Defines arguments for the event +/// +public class MenuOpenedEventArgs : EventArgs { + /// + /// Creates a new instance of the class + /// + /// + /// + public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem) + { + Parent = parent; + MenuItem = menuItem; + } + + /// + /// The parent of . Will be null if menu opening + /// is the root. + /// + public MenuBarItem Parent { get; } + + /// + /// Gets the being opened. + /// + public MenuItem MenuItem { get; } +} + +/// +/// An which allows passing a cancelable menu closing event. +/// +public class MenuClosingEventArgs : EventArgs { + /// + /// The current parent. + /// + public MenuBarItem CurrentMenu { get; } + + /// + /// Indicates whether the current menu will reopen. + /// + public bool Reopen { get; } + + /// + /// Indicates whether the current menu is a sub-menu. + /// + public bool IsSubMenu { get; } + + /// + /// Flag that allows the cancellation of the event. If set to in the + /// event handler, the event will be canceled. + /// + public bool Cancel { get; set; } + + /// + /// Initializes a new instance of . + /// + /// The current parent. + /// Whether the current menu will reopen. + /// Indicates whether it is a sub-menu. + public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu) + { + CurrentMenu = currentMenu; + Reopen = reopen; + IsSubMenu = isSubMenu; + } +} diff --git a/Terminal.Gui/Views/MenuEventArgs.cs b/Terminal.Gui/Views/MenuEventArgs.cs deleted file mode 100644 index 6f3e4c3d30..0000000000 --- a/Terminal.Gui/Views/MenuEventArgs.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; - -namespace Terminal.Gui { - /// - /// An which allows passing a cancelable menu opening event or replacing with a new . - /// - public class MenuOpeningEventArgs : EventArgs { - /// - /// The current parent. - /// - public MenuBarItem CurrentMenu { get; } - - /// - /// The new to be replaced. - /// - public MenuBarItem NewMenuBarItem { get; set; } - /// - /// Flag that allows the cancellation of the event. If set to in the - /// event handler, the event will be canceled. - /// - public bool Cancel { get; set; } - - /// - /// Initializes a new instance of . - /// - /// The current parent. - public MenuOpeningEventArgs (MenuBarItem currentMenu) - { - CurrentMenu = currentMenu; - } - } - - /// - /// Defines arguments for the event - /// - public class MenuOpenedEventArgs : EventArgs { - /// - /// Creates a new instance of the class - /// - /// - /// - public MenuOpenedEventArgs (MenuBarItem parent, MenuItem menuItem) - { - Parent = parent; - MenuItem = menuItem; - } - - /// - /// The parent of . Will be null if menu opening - /// is the root (see ). - /// - public MenuBarItem Parent { get; } - - /// - /// Gets the being opened. - /// - public MenuItem MenuItem { get; } - } - - /// - /// An which allows passing a cancelable menu closing event. - /// - public class MenuClosingEventArgs : EventArgs { - /// - /// The current parent. - /// - public MenuBarItem CurrentMenu { get; } - - /// - /// Indicates whether the current menu will reopen. - /// - public bool Reopen { get; } - - /// - /// Indicates whether the current menu is a sub-menu. - /// - public bool IsSubMenu { get; } - - /// - /// Flag that allows the cancellation of the event. If set to in the - /// event handler, the event will be canceled. - /// - public bool Cancel { get; set; } - - /// - /// Initializes a new instance of . - /// - /// The current parent. - /// Whether the current menu will reopen. - /// Indicates whether it is a sub-menu. - public MenuClosingEventArgs (MenuBarItem currentMenu, bool reopen, bool isSubMenu) - { - CurrentMenu = currentMenu; - Reopen = reopen; - IsSubMenu = isSubMenu; - } - } -} diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index c72fa9a29b..8045e038f2 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -3,414 +3,409 @@ using System.Collections.Generic; using System.Linq; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time. +/// +public class RadioGroup : View { + int _selected = -1; + int _cursor; + DisplayModeLayout _displayMode; + int _horizontalSpace = 2; + List<(int pos, int length)> _horizontal; + /// - /// Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time. + /// Initializes a new instance of the class using layout. /// - public class RadioGroup : View { - int selected = -1; - int cursor; - DisplayModeLayout displayMode; - int horizontalSpace = 2; - List<(int pos, int length)> horizontal; - - /// - /// Initializes a new instance of the class using layout. - /// - public RadioGroup () : this (radioLabels: new string [] { }) { } - - /// - /// Initializes a new instance of the class using layout. - /// - /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. - /// The index of the item to be selected, the value is clamped to the number of items. - public RadioGroup (string [] radioLabels, int selected = 0) : base () - { - SetInitalProperties (Rect.Empty, radioLabels, selected); - } + public RadioGroup () : this (radioLabels: new string [] { }) { } - /// - /// Initializes a new instance of the class using layout. - /// - /// Boundaries for the radio group. - /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. - /// The index of item to be selected, the value is clamped to the number of items. - public RadioGroup (Rect rect, string [] radioLabels, int selected = 0) : base (rect) - { - SetInitalProperties (rect, radioLabels, selected); - } + /// + /// Initializes a new instance of the class using layout. + /// + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The index of the item to be selected, the value is clamped to the number of items. + public RadioGroup (string [] radioLabels, int selected = 0) : base () + { + SetInitialProperties (Rect.Empty, radioLabels, selected); + } - /// - /// Initializes a new instance of the class using layout. - /// The frame is computed from the provided radio labels. - /// - /// The x coordinate. - /// The y coordinate. - /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. - /// The item to be selected, the value is clamped to the number of items. - public RadioGroup (int x, int y, string [] radioLabels, int selected = 0) : - this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected) - { } - - void SetInitalProperties (Rect rect, string [] radioLabels, int selected) - { - if (radioLabels == null) { - this.radioLabels = new List (); - } else { - this.radioLabels = radioLabels.ToList (); - } + /// + /// Initializes a new instance of the class using layout. + /// + /// Boundaries for the radio group. + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The index of item to be selected, the value is clamped to the number of items. + public RadioGroup (Rect rect, string [] radioLabels, int selected = 0) : base (rect) + { + SetInitialProperties (rect, radioLabels, selected); + } - this.selected = selected; - Frame = rect; - CanFocus = true; - HotKeySpecifier = new Rune ('_'); - - // Things this view knows how to do - AddCommand (Command.LineUp, () => { MoveUp (); return true; }); - AddCommand (Command.LineDown, () => { MoveDown (); return true; }); - AddCommand (Command.TopHome, () => { MoveHome (); return true; }); - AddCommand (Command.BottomEnd, () => { MoveEnd (); return true; }); - AddCommand (Command.Accept, () => { SelectItem (); return true; }); - - // Default keybindings for this view - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); - AddKeyBinding (Key.Space, Command.Accept); - - LayoutStarted += RadioGroup_LayoutStarted; + /// + /// Initializes a new instance of the class using layout. + /// The frame is computed from the provided radio labels. + /// + /// The x coordinate. + /// The y coordinate. + /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. + /// The item to be selected, the value is clamped to the number of items. + public RadioGroup (int x, int y, string [] radioLabels, int selected = 0) : + this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected) + { } + + void SetInitialProperties (Rect rect, string [] radioLabels, int selected) + { + HotKeySpecifier = new Rune ('_'); + + if (radioLabels != null) { + RadioLabels = radioLabels; } - private void RadioGroup_LayoutStarted (object sender, EventArgs e) - { - SetWidthHeight (radioLabels); - } + _selected = selected; + Frame = rect; + CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.LineUp, () => { MoveUp (); return true; }); + AddCommand (Command.LineDown, () => { MoveDown (); return true; }); + AddCommand (Command.TopHome, () => { MoveHome (); return true; }); + AddCommand (Command.BottomEnd, () => { MoveEnd (); return true; }); + AddCommand (Command.Accept, () => { SelectItem (); return true; }); + + // Default keybindings for this view + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.Space, Command.Accept); + + LayoutStarted += RadioGroup_LayoutStarted; + } - /// - /// Gets or sets the for this . - /// - public DisplayModeLayout DisplayMode { - get { return displayMode; } - set { - if (displayMode != value) { - displayMode = value; - SetWidthHeight (radioLabels); - SetNeedsDisplay (); - } + void RadioGroup_LayoutStarted (object sender, EventArgs e) + { + SetWidthHeight (_radioLabels); + } + + /// + /// Gets or sets the for this . + /// + public DisplayModeLayout DisplayMode { + get { return _displayMode; } + set { + if (_displayMode != value) { + _displayMode = value; + SetWidthHeight (_radioLabels); + SetNeedsDisplay (); } } + } - /// - /// Gets or sets the horizontal space for this if the is - /// - public int HorizontalSpace { - get { return horizontalSpace; } - set { - if (horizontalSpace != value && displayMode == DisplayModeLayout.Horizontal) { - horizontalSpace = value; - SetWidthHeight (radioLabels); - UpdateTextFormatterText (); - SetNeedsDisplay (); - } + /// + /// Gets or sets the horizontal space for this if the is + /// + public int HorizontalSpace { + get { return _horizontalSpace; } + set { + if (_horizontalSpace != value && _displayMode == DisplayModeLayout.Horizontal) { + _horizontalSpace = value; + SetWidthHeight (_radioLabels); + UpdateTextFormatterText (); + SetNeedsDisplay (); } } + } - void SetWidthHeight (List radioLabels) - { - switch (displayMode) { - case DisplayModeLayout.Vertical: - var r = MakeRect (0, 0, radioLabels); - Bounds = new Rect (Bounds.Location, new Size (r.Width, radioLabels.Count)); - break; - - case DisplayModeLayout.Horizontal: - CalculateHorizontalPositions (); - var length = 0; - foreach (var item in horizontal) { - length += item.length; - } - var hr = new Rect (0, 0, length, 1); - if (IsAdded && LayoutStyle == LayoutStyle.Computed) { - Width = hr.Width; - Height = 1; - } else { - Bounds = new Rect (Bounds.Location, new Size (hr.Width, radioLabels.Count)); - } - break; + void SetWidthHeight (List radioLabels) + { + switch (_displayMode) { + case DisplayModeLayout.Vertical: + var r = MakeRect (0, 0, radioLabels); + Bounds = new Rect (Bounds.Location, new Size (r.Width, radioLabels.Count)); + break; + + case DisplayModeLayout.Horizontal: + CalculateHorizontalPositions (); + var length = 0; + foreach (var item in _horizontal) { + length += item.length; + } + var hr = new Rect (0, 0, length, 1); + if (IsAdded && LayoutStyle == LayoutStyle.Computed) { + Width = hr.Width; + Height = 1; + } else { + Bounds = new Rect (Bounds.Location, new Size (hr.Width, radioLabels.Count)); } + break; } + } - static Rect MakeRect (int x, int y, List radioLabels) - { - if (radioLabels == null) { - return new Rect (x, y, 0, 0); - } + static Rect MakeRect (int x, int y, List radioLabels) + { + if (radioLabels == null) { + return new Rect (x, y, 0, 0); + } - int width = 0; + int width = 0; - foreach (var s in radioLabels) { - width = Math.Max (s.GetColumns () + 2, width); - } - return new Rect (x, y, width, radioLabels.Count); + foreach (var s in radioLabels) { + width = Math.Max (s.GetColumns () + 2, width); } + return new Rect (x, y, width, radioLabels.Count); + } - List radioLabels = new List (); - - /// - /// The radio labels to display - /// - /// The radio labels. - public string [] RadioLabels { - get => radioLabels.ToArray (); - set { - var prevCount = radioLabels.Count; - radioLabels = value.ToList (); - if (prevCount != radioLabels.Count) { - SetWidthHeight (radioLabels); + List _radioLabels = new List (); + + /// + /// The radio labels to display. A key binding will be added for each radio radio enabling the user + /// to select and/or focus the radio label using the keyboard. See for details + /// on how HotKeys work. + /// + /// The radio labels. + public string [] RadioLabels { + get => _radioLabels.ToArray (); + set { + // Remove old hot key bindings + foreach (var label in _radioLabels) { + if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) { + AddKeyBindingsForHotKey (hotKey, KeyCode.Null); } - SelectedItem = 0; - cursor = 0; - SetNeedsDisplay (); } - } - - private void CalculateHorizontalPositions () - { - if (displayMode == DisplayModeLayout.Horizontal) { - horizontal = new List<(int pos, int length)> (); - int start = 0; - int length = 0; - for (int i = 0; i < radioLabels.Count; i++) { - start += length; - length = radioLabels [i].GetColumns () + 2 + (i < radioLabels.Count - 1 ? horizontalSpace : 0); - horizontal.Add ((start, length)); + var prevCount = _radioLabels.Count; + _radioLabels = value.ToList (); + foreach (var label in _radioLabels) { + if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) { + AddKeyBindingsForHotKey (KeyCode.Null, hotKey); } } + if (prevCount != _radioLabels.Count) { + SetWidthHeight (_radioLabels); + } + SelectedItem = 0; + _cursor = 0; + SetNeedsDisplay (); } + } - /// - public override void OnDrawContent (Rect contentArea) - { - base.OnDrawContent (contentArea); - - Driver.SetAttribute (GetNormalColor ()); - for (int i = 0; i < radioLabels.Count; i++) { - switch (DisplayMode) { - case DisplayModeLayout.Vertical: - Move (0, i); - break; - case DisplayModeLayout.Horizontal: - Move (horizontal [i].pos, 0); + /// + public override bool? OnInvokingKeyBindings (Key keyEvent) + { + // This is a bit of a hack. We want to handle the key bindings for the radio group but + // InvokeKeyBindings doesn't pass any context so we can't tell if the key binding is for + // the radio group or for one of the radio buttons. So before we call the base class + // we set SelectedItem appropriately. + + var key = keyEvent; + if (KeyBindings.TryGet (key, out _)) { + // Search RadioLabels + for (int i = 0; i < _radioLabels.Count; i++) { + if (TextFormatter.FindHotKey (_radioLabels [i], HotKeySpecifier, true, out _, out var hotKey) + && (key.NoAlt.NoCtrl.NoShift) == hotKey) { + SelectedItem = i; + keyEvent.Scope = KeyBindingScope.HotKey; break; } - var rl = radioLabels [i]; - Driver.SetAttribute (GetNormalColor ()); - Driver.AddStr ($"{(i == selected ? CM.Glyphs.Selected : CM.Glyphs.UnSelected)} "); - TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out Key hotKey); - if (hotPos != -1 && (hotKey != Key.Null || hotKey != Key.Unknown)) { - var rlRunes = rl.ToRunes (); - for (int j = 0; j < rlRunes.Length; j++) { - Rune rune = rlRunes [j]; - if (j == hotPos && i == cursor) { - Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); - } else if (j == hotPos && i != cursor) { - Application.Driver.SetAttribute (GetHotNormalColor ()); - } else if (HasFocus && i == cursor) { - Application.Driver.SetAttribute (ColorScheme.Focus); - } - if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) { - j++; - rune = rlRunes [j]; - if (i == cursor) { - Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); - } else if (i != cursor) { - Application.Driver.SetAttribute (GetHotNormalColor ()); - } - } - Application.Driver.AddRune (rune); - Driver.SetAttribute (GetNormalColor ()); - } - } else { - DrawHotString (rl, HasFocus && i == cursor, ColorScheme); - } + } + + } + return base.OnInvokingKeyBindings (keyEvent); + } + + void CalculateHorizontalPositions () + { + if (_displayMode == DisplayModeLayout.Horizontal) { + _horizontal = new List<(int pos, int length)> (); + int start = 0; + int length = 0; + for (int i = 0; i < _radioLabels.Count; i++) { + start += length; + length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0); + _horizontal.Add ((start, length)); } } + } - /// - public override void PositionCursor () - { + /// + public override void OnDrawContent (Rect contentArea) + { + base.OnDrawContent (contentArea); + + Driver.SetAttribute (GetNormalColor ()); + for (int i = 0; i < _radioLabels.Count; i++) { switch (DisplayMode) { case DisplayModeLayout.Vertical: - Move (0, cursor); + Move (0, i); break; case DisplayModeLayout.Horizontal: - Move (horizontal [cursor].pos, 0); + Move (_horizontal [i].pos, 0); break; } - } - - /// - /// Invoked when the selected radio label has changed. - /// - public event EventHandler SelectedItemChanged; - - /// - /// The currently selected item from the list of radio labels - /// - /// The selected. - public int SelectedItem { - get => selected; - set { - OnSelectedItemChanged (value, SelectedItem); - cursor = selected; - SetNeedsDisplay (); + var rl = _radioLabels [i]; + Driver.SetAttribute (GetNormalColor ()); + Driver.AddStr ($"{(i == _selected ? CM.Glyphs.Selected : CM.Glyphs.UnSelected)} "); + TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out var hotKey); + if (hotPos != -1 && (hotKey != KeyCode.Null || hotKey != KeyCode.Unknown)) { + var rlRunes = rl.ToRunes (); + for (int j = 0; j < rlRunes.Length; j++) { + Rune rune = rlRunes [j]; + if (j == hotPos && i == _cursor) { + Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); + } else if (j == hotPos && i != _cursor) { + Application.Driver.SetAttribute (GetHotNormalColor ()); + } else if (HasFocus && i == _cursor) { + Application.Driver.SetAttribute (ColorScheme.Focus); + } + if (rune == HotKeySpecifier && j + 1 < rlRunes.Length) { + j++; + rune = rlRunes [j]; + if (i == _cursor) { + Application.Driver.SetAttribute (HasFocus ? ColorScheme.HotFocus : GetHotNormalColor ()); + } else if (i != _cursor) { + Application.Driver.SetAttribute (GetHotNormalColor ()); + } + } + Application.Driver.AddRune (rune); + Driver.SetAttribute (GetNormalColor ()); + } + } else { + DrawHotString (rl, HasFocus && i == _cursor, ColorScheme); } } + } - /// - /// Allow to invoke the after their creation. - /// - public void Refresh () - { - OnSelectedItemChanged (selected, -1); + /// + public override void PositionCursor () + { + switch (DisplayMode) { + case DisplayModeLayout.Vertical: + Move (0, _cursor); + break; + case DisplayModeLayout.Horizontal: + Move (_horizontal [_cursor].pos, 0); + break; } + } - /// - /// Called whenever the current selected item changes. Invokes the event. - /// - /// - /// - public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem) - { - selected = selectedItem; - SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem)); - } + /// + /// Invoked when the selected radio label has changed. + /// + public event EventHandler SelectedItemChanged; - /// - public override bool ProcessColdKey (KeyEvent kb) - { - var key = kb.KeyValue; - if (key < Char.MaxValue && Char.IsLetterOrDigit ((char)key)) { - int i = 0; - key = Char.ToUpper ((char)key); - foreach (var l in radioLabels) { - bool nextIsHot = false; - TextFormatter.FindHotKey (l, HotKeySpecifier, true, out _, out Key hotKey); - foreach (Rune c in l) { - if (c == HotKeySpecifier) { - nextIsHot = true; - } else { - if ((nextIsHot && Rune.ToUpperInvariant (c).Value == key) || (key == (uint)hotKey)) { - SelectedItem = i; - cursor = i; - if (!HasFocus) - SetFocus (); - return true; - } - nextIsHot = false; - } - } - i++; - } - } - return false; + /// + /// The currently selected item from the list of radio labels + /// + /// The selected. + public int SelectedItem { + get => _selected; + set { + OnSelectedItemChanged (value, SelectedItem); + _cursor = _selected; + SetNeedsDisplay (); } + } - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; + /// + /// Allow to invoke the after their creation. + /// + public void Refresh () + { + OnSelectedItemChanged (_selected, -1); + } - return base.ProcessKey (kb); - } + /// + /// Called whenever the current selected item changes. Invokes the event. + /// + /// + /// + public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem) + { + _selected = selectedItem; + SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem)); + } - void SelectItem () - { - SelectedItem = cursor; - } + void SelectItem () + { + SelectedItem = _cursor; + } - void MoveEnd () - { - cursor = Math.Max (radioLabels.Count - 1, 0); - } + void MoveEnd () + { + _cursor = Math.Max (_radioLabels.Count - 1, 0); + } - void MoveHome () - { - cursor = 0; - } + void MoveHome () + { + _cursor = 0; + } - void MoveDown () - { - if (cursor + 1 < radioLabels.Count) { - cursor++; - SetNeedsDisplay (); - } else if (cursor > 0) { - cursor = 0; - SetNeedsDisplay (); - } + void MoveDown () + { + if (_cursor + 1 < _radioLabels.Count) { + _cursor++; + SetNeedsDisplay (); + } else if (_cursor > 0) { + _cursor = 0; + SetNeedsDisplay (); } + } - void MoveUp () - { - if (cursor > 0) { - cursor--; - SetNeedsDisplay (); - } else if (radioLabels.Count - 1 > 0) { - cursor = radioLabels.Count - 1; - SetNeedsDisplay (); - } + void MoveUp () + { + if (_cursor > 0) { + _cursor--; + SetNeedsDisplay (); + } else if (_radioLabels.Count - 1 > 0) { + _cursor = _radioLabels.Count - 1; + SetNeedsDisplay (); } + } - /// - public override bool MouseEvent (MouseEvent me) - { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) { - return false; - } - if (!CanFocus) { - return false; - } - SetFocus (); + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) { + return false; + } + if (!CanFocus) { + return false; + } + SetFocus (); - int boundsX = me.X; - int boundsY = me.Y; + int boundsX = me.X; + int boundsY = me.Y; - var pos = displayMode == DisplayModeLayout.Horizontal ? boundsX : boundsY; - var rCount = displayMode == DisplayModeLayout.Horizontal ? horizontal.Last ().pos + horizontal.Last ().length : radioLabels.Count; + var pos = _displayMode == DisplayModeLayout.Horizontal ? boundsX : boundsY; + var rCount = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.Last ().pos + _horizontal.Last ().length : _radioLabels.Count; - if (pos < rCount) { - var c = displayMode == DisplayModeLayout.Horizontal ? horizontal.FindIndex ((x) => x.pos <= boundsX && x.pos + x.length - 2 >= boundsX) : boundsY; - if (c > -1) { - cursor = SelectedItem = c; - SetNeedsDisplay (); - } + if (pos < rCount) { + var c = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.FindIndex ((x) => x.pos <= boundsX && x.pos + x.length - 2 >= boundsX) : boundsY; + if (c > -1) { + _cursor = SelectedItem = c; + SetNeedsDisplay (); } - return true; } + return true; + } - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - return base.OnEnter (view); - } + return base.OnEnter (view); } +} +/// +/// Used for choose the display mode of this +/// +public enum DisplayModeLayout { /// - /// Used for choose the display mode of this + /// Vertical mode display. It's the default. /// - public enum DisplayModeLayout { - /// - /// Vertical mode display. It's the default. - /// - Vertical, - /// - /// Horizontal mode display. - /// - Horizontal - } + Vertical, + /// + /// Horizontal mode display. + /// + Horizontal } diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index e4b8f3a1a5..02b57510e9 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -104,23 +104,23 @@ void SetInitialProperties (Rect frame) AddCommand (Command.RightEnd, () => ScrollRight (_contentSize.Width)); // Default keybindings for this view - AddKeyBinding (Key.CursorUp, Command.ScrollUp); - AddKeyBinding (Key.CursorDown, Command.ScrollDown); - AddKeyBinding (Key.CursorLeft, Command.ScrollLeft); - AddKeyBinding (Key.CursorRight, Command.ScrollRight); + KeyBindings.Add (KeyCode.CursorUp, Command.ScrollUp); + KeyBindings.Add (KeyCode.CursorDown, Command.ScrollDown); + KeyBindings.Add (KeyCode.CursorLeft, Command.ScrollLeft); + KeyBindings.Add (KeyCode.CursorRight, Command.ScrollRight); - AddKeyBinding (Key.PageUp, Command.PageUp); - AddKeyBinding ((Key)'v' | Key.AltMask, Command.PageUp); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add ((KeyCode)'v' | KeyCode.AltMask, Command.PageUp); - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.PageDown); - AddKeyBinding (Key.PageUp | Key.CtrlMask, Command.PageLeft); - AddKeyBinding (Key.PageDown | Key.CtrlMask, Command.PageRight); - AddKeyBinding (Key.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); - AddKeyBinding (Key.Home | Key.CtrlMask, Command.LeftHome); - AddKeyBinding (Key.End | Key.CtrlMask, Command.RightEnd); + KeyBindings.Add (KeyCode.PageUp | KeyCode.CtrlMask, Command.PageLeft); + KeyBindings.Add (KeyCode.PageDown | KeyCode.CtrlMask, Command.PageRight); + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask, Command.LeftHome); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask, Command.RightEnd); Initialized += (s, e) => { if (!_vertical.IsInitialized) { @@ -563,12 +563,12 @@ public bool ScrollRight (int cols) } /// - public override bool ProcessKey (KeyEvent kb) + public override bool OnKeyDown (Key a) { - if (base.ProcessKey (kb)) + if (base.OnKeyDown (a)) return true; - var result = InvokeKeybindings (kb); + var result = InvokeKeyBindings (a); if (result != null) return (bool)result; diff --git a/Terminal.Gui/Views/Slider.cs b/Terminal.Gui/Views/Slider.cs index 52bc5c03ee..2b161bc297 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -1403,48 +1403,34 @@ void SetCommands () void SetKeyBindings () { if (_config._sliderOrientation == Orientation.Horizontal) { - AddKeyBinding (Key.CursorRight, Command.Right); - ClearKeyBinding (Key.CursorDown); - AddKeyBinding (Key.CursorLeft, Command.Left); - ClearKeyBinding (Key.CursorUp); - - AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.RightExtend); - ClearKeyBinding (Key.CursorDown | Key.CtrlMask); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.LeftExtend); - ClearKeyBinding (Key.CursorUp | Key.CtrlMask); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Remove (KeyCode.CursorDown); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Remove (KeyCode.CursorUp); + + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.RightExtend); + KeyBindings.Remove (KeyCode.CursorDown | KeyCode.CtrlMask); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.LeftExtend); + KeyBindings.Remove (KeyCode.CursorUp | KeyCode.CtrlMask); } else { - ClearKeyBinding (Key.CursorRight); - AddKeyBinding (Key.CursorDown, Command.LineDown); - ClearKeyBinding (Key.CursorLeft); - AddKeyBinding (Key.CursorUp, Command.LineUp); + KeyBindings.Remove (KeyCode.CursorRight); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Remove (KeyCode.CursorLeft); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); - ClearKeyBinding (Key.CursorRight | Key.CtrlMask); - AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.RightExtend); - ClearKeyBinding (Key.CursorLeft | Key.CtrlMask); - AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.LeftExtend); + KeyBindings.Remove (KeyCode.CursorRight | KeyCode.CtrlMask); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.RightExtend); + KeyBindings.Remove (KeyCode.CursorLeft | KeyCode.CtrlMask); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LeftExtend); } - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.End, Command.RightEnd); - AddKeyBinding (Key.Enter, Command.Accept); - AddKeyBinding (Key.Space, Command.Accept); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.End, Command.RightEnd); + KeyBindings.Add (KeyCode.Enter, Command.Accept); + KeyBindings.Add (KeyCode.Space, Command.Accept); } - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (!CanFocus || !HasFocus) { - return base.ProcessKey (keyEvent); - } - - var result = InvokeKeybindings (keyEvent); - if (result != null) { - return (bool)result; - } - return base.ProcessKey (keyEvent); - } - Dictionary> GetSetOptionDictionary () => _setOptions.ToDictionary (e => e, e => _options [e]); void SetFocusedOption () diff --git a/Terminal.Gui/Views/StatusBar.cs b/Terminal.Gui/Views/StatusBar.cs index 1d66b853dd..4fdd0b6c94 100644 --- a/Terminal.Gui/Views/StatusBar.cs +++ b/Terminal.Gui/Views/StatusBar.cs @@ -1,273 +1,296 @@ -// -// StatusBar.cs: a statusbar for an application -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -// TODO: -// Add mouse support using System; using System.Collections.Generic; using System.Text; -namespace Terminal.Gui { +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 { /// - /// 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. + /// 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 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 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; - } + /// + public string Title { get; set; } - /// - /// Gets the global shortcut to invoke the action on the menu. - /// - public Key Shortcut { get; set; } + /// + /// Gets or sets the action to be invoked when the statusbar item is triggered + /// + /// Action to invoke. + public Action Action { 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; } + /// + /// 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 the action to be invoked when the statusbar item is triggered - /// - /// Action to invoke. - public Action Action { get; set; } + /// + /// Returns if the status item is enabled. This method is a wrapper around . + /// + public bool IsEnabled () + { + return CanExecute?.Invoke () ?? true; + } - /// - /// 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; } +}; - /// - /// Returns if the status item is enabled. This method is a wrapper around . - /// - public bool IsEnabled () - { - return CanExecute == null ? true : CanExecute (); +/// +/// 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 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 { + /// + /// The items that compose the + /// + public StatusItem [] Items { + get => _items; + set { + foreach (var item in _items) { + KeyBindings.Remove ((KeyCode)item.Shortcut); + } + _items = value; + foreach (var item in _items) { + KeyBindings.Add ((KeyCode)item.Shortcut, KeyBindingScope.HotKey, Command.Accept); + } } - - /// - /// Gets or sets arbitrary data for the status item. - /// - /// This property is not used internally. - public object Data { get; set; } - }; + } /// - /// 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 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 statusbar. + /// Initializes a new instance of the class. /// - public class StatusBar : View { - /// - /// The items that compose the - /// - public StatusItem [] Items { get; set; } - - /// - /// Initializes a new instance of the class. - /// - public StatusBar () : this (items: new StatusItem [] { }) { } + public StatusBar () : this (items: 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 statusbar items. - public StatusBar (StatusItem [] items) : base () - { + /// + /// 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) : base () + { + if (items != null) { Items = items; - CanFocus = false; - ColorScheme = Colors.Menu; - X = 0; - Y = Pos.AnchorEnd (1); - Width = Dim.Fill (); - Height = 1; } + CanFocus = false; + ColorScheme = Colors.Menu; + X = 0; + Y = Pos.AnchorEnd (1); + Width = Dim.Fill (); + Height = 1; + AddCommand (Command.Accept, InvokeItem); + } + + StatusItem _itemToInvoke; + bool? InvokeItem () + { + if (_itemToInvoke is { Action: not null }) { + _itemToInvoke.Action.Invoke (); + return true; + } + return false; + } - static string shortcutDelimiter = "-"; - /// - /// Used for change the shortcut delimiter separator. - /// - public static string ShortcutDelimiter { - get => shortcutDelimiter; - set { - if (shortcutDelimiter != value) { - shortcutDelimiter = value == string.Empty ? " " : value; + /// + public override bool? OnInvokingKeyBindings (Key keyEvent) + { + // This is a bit of a hack. We want to handle the key bindings for status bar but + // InvokeKeyBindings doesn't pass any context so we can't tell which item it is for. + // So before we call the base class we set SelectedItem appropriately. + var key = keyEvent.KeyCode; + if (KeyBindings.TryGet(key, out _)) { + // Search RadioLabels + foreach (var item in Items) { + if (item.Shortcut == key) { + _itemToInvoke = item; + keyEvent.Scope = KeyBindingScope.HotKey; + break; } } - } - Attribute ToggleScheme (Attribute scheme) - { - var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; - Driver.SetAttribute (result); - return result; } + return base.OnInvokingKeyBindings (keyEvent); + } + static Rune _shortcutDelimiter = (Rune)'='; + StatusItem [] _items = new StatusItem [] { }; - Attribute DetermineColorSchemeFor (StatusItem item) - { - if (item != null) { - if (item.IsEnabled ()) { - return GetNormalColor (); - } - return ColorScheme.Disabled; + /// + /// Gets or sets shortcut delimiter separator. The default is "-". + /// + public static Rune ShortcutDelimiter { + get => _shortcutDelimiter; + set { + if (_shortcutDelimiter != value) { + _shortcutDelimiter = value == default ? (Rune)'=' : value; } - return GetNormalColor (); } + } - /// - public override void OnDrawContent (Rect contentArea) - { - Move (0, 0); - Driver.SetAttribute (GetNormalColor ()); - for (int i = 0; i < Frame.Width; i++) { - Driver.AddRune ((Rune)' '); - } + Attribute ToggleScheme (Attribute scheme) + { + var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; + Driver.SetAttribute (result); + return result; + } - Move (1, 0); - var scheme = GetNormalColor (); - Driver.SetAttribute (scheme); - for (int i = 0; i < Items.Length; i++) { - var title = Items [i].Title; - Driver.SetAttribute (DetermineColorSchemeFor (Items [i])); - for (int 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 (CM.Glyphs.VLine); - Driver.AddRune ((Rune)' '); - } + Attribute DetermineColorSchemeFor (StatusItem item) + { + if (item != null) { + if (item.IsEnabled ()) { + return GetNormalColor (); } + return ColorScheme.Disabled; } + return GetNormalColor (); + } - /// - public override bool ProcessHotKey (KeyEvent kb) - { - foreach (var item in Items) { - if (kb.Key == item.Shortcut) { - if (item.IsEnabled ()) { - Run (item.Action); - } - return true; - } - } - return false; + /// + public override void OnDrawContent (Rect contentArea) + { + Move (0, 0); + Driver.SetAttribute (GetNormalColor ()); + for (int i = 0; i < Frame.Width; i++) { + Driver.AddRune ((Rune)' '); } - /// - public override bool MouseEvent (MouseEvent me) - { - if (me.Flags != MouseFlags.Button1Clicked) - return false; - - int pos = 1; - for (int i = 0; i < Items.Length; i++) { - if (me.X >= pos && me.X < pos + GetItemTitleLength (Items [i].Title)) { - var item = Items [i]; - if (item.IsEnabled ()) { - Run (item.Action); + Move (1, 0); + var scheme = GetNormalColor (); + Driver.SetAttribute (scheme); + for (int i = 0; i < Items.Length; i++) { + var title = Items [i].Title; + Driver.SetAttribute (DetermineColorSchemeFor (Items [i])); + for (int n = 0; n < Items [i].Title.GetRuneCount (); n++) { + if (title [n] == '~') { + if (Items [i].IsEnabled ()) { + scheme = ToggleScheme (scheme); } - break; + continue; } - pos += GetItemTitleLength (Items [i].Title) + 3; + Driver.AddRune ((Rune)title [n]); + } + if (i + 1 < Items.Length) { + Driver.AddRune ((Rune)' '); + Driver.AddRune (CM.Glyphs.VLine); + Driver.AddRune ((Rune)' '); } - return true; } + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (me.Flags != MouseFlags.Button1Clicked) + return false; - int GetItemTitleLength (string title) - { - int len = 0; - foreach (var ch in title) { - if (ch == '~') - continue; - len++; + int pos = 1; + for (int i = 0; i < Items.Length; i++) { + if (me.X >= pos && me.X < pos + GetItemTitleLength (Items [i].Title)) { + var item = Items [i]; + if (item.IsEnabled ()) { + Run (item.Action); + } + break; } + pos += GetItemTitleLength (Items [i].Title) + 3; + } + return true; + } - return len; + int GetItemTitleLength (string title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '~') + continue; + len++; } - void Run (Action action) - { - if (action == null) - return; + return len; + } - Application.MainLoop.AddIdle (() => { - action (); - return false; - }); - } + void Run (Action action) + { + if (action == null) + return; + + Application.MainLoop.AddIdle (() => { + action (); + return false; + }); + } - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - return base.OnEnter (view); - } + return base.OnEnter (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) - { - var itemsList = new List (Items); - itemsList.Insert (index, item); - Items = itemsList.ToArray (); - SetNeedsDisplay (); - } + /// + /// 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) + { + var itemsList = new List (Items); + itemsList.Insert (index, item); + Items = itemsList.ToArray (); + SetNeedsDisplay (); + } - /// - /// Removes a at specified index of . - /// - /// The zero-based index of the item to remove. - /// The removed. - public StatusItem RemoveItem (int index) - { - var itemsList = new List (Items); - var item = itemsList [index]; - itemsList.RemoveAt (index); - Items = itemsList.ToArray (); - SetNeedsDisplay (); + /// + /// Removes a at specified index of . + /// + /// The zero-based index of the item to remove. + /// The removed. + public StatusItem RemoveItem (int index) + { + var itemsList = new List (Items); + var item = itemsList [index]; + itemsList.RemoveAt (index); + Items = itemsList.ToArray (); + SetNeedsDisplay (); - return item; - } + return item; } -} \ No newline at end of file +} diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 86a558b82d..9b8c80bca3 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -123,10 +123,10 @@ public TabView () : base () AddCommand (Command.RightEnd, () => { SelectedTab = Tabs.LastOrDefault (); return true; }); // Default keybindings for this view - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.End, Command.RightEnd); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.End, Command.RightEnd); } /// @@ -229,18 +229,6 @@ protected virtual void OnSelectedTabChanged (Tab oldTab, Tab newTab) SelectedTabChanged?.Invoke (this, new TabChangedEventArgs (oldTab, newTab)); } - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (HasFocus && CanFocus && Focused == tabsBar) { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; - } - - return base.ProcessKey (keyEvent); - } - /// /// Changes the by the given . /// Positive for right, negative for left. If no tab is currently selected then diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs index 4057279556..3e59a118e7 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -29,7 +29,7 @@ public CheckBoxTableSourceWrapperBase (TableView tableView, ITableSource toWrap) this.Wrapping = toWrap; this.tableView = tableView; - tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); + tableView.KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); tableView.MouseClick += TableView_MouseClick; tableView.CellToggled += TableView_CellToggled; diff --git a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs index b6d8116396..736fef46a0 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapperByObject.cs @@ -1,76 +1,75 @@ using System; using System.Linq; -namespace Terminal.Gui { +namespace Terminal.Gui; +/// +/// Implementation of which records toggled rows +/// by a property on row objects. +/// +public class CheckBoxTableSourceWrapperByObject : CheckBoxTableSourceWrapperBase { + private readonly IEnumerableTableSource _toWrap; + readonly Func _getter; + readonly Action _setter; + /// - /// Implementation of which records toggled rows - /// by a property on row objects. + /// Creates a new instance of the class wrapping the collection . /// - public class CheckBoxTableSourceWrapperByObject : CheckBoxTableSourceWrapperBase { - private readonly IEnumerableTableSource toWrap; - readonly Func getter; - readonly Action setter; + /// The table you will use the source with. + /// The collection of objects you will record checked state for + /// Delegate method for retrieving checked state from your objects of type . + /// Delegate method for setting new checked states on your objects of type . + public CheckBoxTableSourceWrapperByObject ( + TableView tableView, + IEnumerableTableSource toWrap, + Func getter, + Action setter) : base (tableView, toWrap) + { + this._toWrap = toWrap; + this._getter = getter; + this._setter = setter; + } - /// - /// Creates a new instance of the class wrapping the collection . - /// - /// The table you will use the source with. - /// The collection of objects you will record checked state for - /// Delegate method for retrieving checked state from your objects of type . - /// Delegate method for setting new checked states on your objects of type . - public CheckBoxTableSourceWrapperByObject ( - TableView tableView, - IEnumerableTableSource toWrap, - Func getter, - Action setter) : base (tableView, toWrap) - { - this.toWrap = toWrap; - this.getter = getter; - this.setter = setter; - } + /// + protected override bool IsChecked (int row) + { + return _getter (_toWrap.GetObjectOnRow (row)); + } - /// - protected override bool IsChecked (int row) - { - return getter (toWrap.GetObjectOnRow (row)); - } + /// + protected override void ToggleAllRows () + { + ToggleRows (Enumerable.Range (0, _toWrap.Rows).ToArray ()); + } - /// - protected override void ToggleAllRows () - { - ToggleRows (Enumerable.Range (0, toWrap.Rows).ToArray()); - } + /// + protected override void ToggleRow (int row) + { + var d = _toWrap.GetObjectOnRow (row); + _setter (d, !_getter (d)); + } - /// - protected override void ToggleRow (int row) - { - var d = toWrap.GetObjectOnRow (row); - setter (d, !getter(d)); - } - - /// - protected override void ToggleRows (int [] range) - { - // if all are ticked untick them - if (range.All (IsChecked)) { - // select none - foreach(var r in range) { - setter (toWrap.GetObjectOnRow (r), false); - } - } else { - // otherwise tick all - foreach (var r in range) { - setter (toWrap.GetObjectOnRow (r), true); - } + /// + protected override void ToggleRows (int [] range) + { + // if all are ticked untick them + if (range.All (IsChecked)) { + // select none + foreach (var r in range) { + _setter (_toWrap.GetObjectOnRow (r), false); + } + } else { + // otherwise tick all + foreach (var r in range) { + _setter (_toWrap.GetObjectOnRow (r), true); } } + } - /// - protected override void ClearAllToggles () - { - foreach (var e in toWrap.GetAllObjects()) { - setter (e, false); - } + /// + protected override void ClearAllToggles () + { + foreach (var e in _toWrap.GetAllObjects ()) { + _setter (e, false); } } } diff --git a/Terminal.Gui/Views/TableView/ColumnStyle.cs b/Terminal.Gui/Views/TableView/ColumnStyle.cs index 079cafae09..80e07ca2ad 100644 --- a/Terminal.Gui/Views/TableView/ColumnStyle.cs +++ b/Terminal.Gui/Views/TableView/ColumnStyle.cs @@ -6,7 +6,7 @@ namespace Terminal.Gui; /// Describes how to render a given column in a including /// and textual representation of cells (e.g. date formats) /// -/// See TableView Deep Dive for more information. +/// See TableView Deep Dive for more information. /// public class ColumnStyle { diff --git a/Terminal.Gui/Views/TableView/TableStyle.cs b/Terminal.Gui/Views/TableView/TableStyle.cs index 7fc3d6b99e..35479ed979 100644 --- a/Terminal.Gui/Views/TableView/TableStyle.cs +++ b/Terminal.Gui/Views/TableView/TableStyle.cs @@ -5,7 +5,7 @@ namespace Terminal.Gui; /// /// Defines rendering options that affect how the table is displayed. /// -/// See TableView Deep Dive for more information. +/// See TableView Deep Dive for more information. /// public class TableStyle { diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index f9a21a3446..c04d1252bd 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -24,7 +24,7 @@ namespace Terminal.Gui { /// /// View for tabular data based on a . /// - /// See TableView Deep Dive for more information. + /// See TableView Deep Dive for more information. /// public class TableView : View { @@ -34,7 +34,8 @@ public class TableView : View { private int selectedColumn; private ITableSource table; private TableStyle style = new TableStyle (); - private Key cellActivationKey = Key.Enter; + // TODO: Update to use Key instead of KeyCode + private KeyCode cellActivationKey = KeyCode.Enter; Point? scrollLeftPoint; Point? scrollRightPoint; @@ -169,18 +170,19 @@ public int SelectedRow { /// public event EventHandler CellToggled; + // TODO: Update to use Key instead of KeyCode /// /// The key which when pressed should trigger event. Defaults to Enter. /// - public Key CellActivationKey { + public KeyCode CellActivationKey { get => cellActivationKey; set { if (cellActivationKey != value) { - ReplaceKeyBinding (cellActivationKey, value); + KeyBindings.Replace (cellActivationKey, value); // of API user is mixing and matching old and new methods of keybinding then they may have lost - // the old binding (e.g. with ClearKeybindings) so ReplaceKeyBinding alone will fail - AddKeyBinding (value, Command.Accept); + // the old binding (e.g. with ClearKeybindings) so KeyBindings.Replace alone will fail + KeyBindings.Add (value, Command.Accept); cellActivationKey = value; } } @@ -239,30 +241,30 @@ public TableView () : base () AddCommand (Command.ToggleChecked, () => { ToggleCurrentCellSelection (); return true; }); // Default keybindings for this view - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.PageUp, Command.PageUp); - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.End, Command.RightEnd); - AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome); - AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd); - - AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); - AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); - AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); - AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); - AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); - AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); - AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend); - AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend); - AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend); - AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend); - - AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll); - AddKeyBinding (CellActivationKey, Command.Accept); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.End, Command.RightEnd); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask, Command.TopHome); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask, Command.BottomEnd); + + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask, Command.LeftExtend); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask, Command.RightExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend); + KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend); + KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask, Command.LeftHomeExtend); + KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask, Command.RightEndExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.TopHomeExtend); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.BottomEndExtend); + + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll); + KeyBindings.Add (CellActivationKey, Command.Accept); } /// @@ -758,36 +760,29 @@ private string TruncateOrPad (object originalCellValue, string representation, i return new string (representation.TakeWhile (c => (availableHorizontalSpace -= ((Rune)c).GetColumns ()) > 0).ToArray ()); } - - /// - public override bool ProcessKey (KeyEvent keyEvent) + public override bool OnProcessKeyDown (Key keyEvent) { if (TableIsNullOrInvisible ()) { PositionCursor (); return false; } - var result = InvokeKeybindings (keyEvent); - if (result != null) { - PositionCursor (); - return true; - } - + if (CollectionNavigator != null && this.HasFocus && Table.Rows != 0 && Terminal.Gui.CollectionNavigator.IsCompatibleKey (keyEvent) && - !keyEvent.Key.HasFlag (Key.CtrlMask) && - !keyEvent.Key.HasFlag (Key.AltMask) && - char.IsLetterOrDigit ((char)keyEvent.KeyValue)) { + !keyEvent.KeyCode.HasFlag (KeyCode.CtrlMask) && + !keyEvent.KeyCode.HasFlag (KeyCode.AltMask) && + Rune.IsLetterOrDigit ((Rune)keyEvent)) { return CycleToNextTableEntryBeginningWith (keyEvent); } return false; } - private bool CycleToNextTableEntryBeginningWith (KeyEvent keyEvent) + private bool CycleToNextTableEntryBeginningWith (Key keyEvent) { var row = SelectedRow; @@ -796,7 +791,7 @@ private bool CycleToNextTableEntryBeginningWith (KeyEvent keyEvent) return false; } - int match = CollectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyValue); + int match = CollectionNavigator.GetNextMatchingItem (row, (char)keyEvent); if (match != -1) { SelectedRow = match; @@ -1291,7 +1286,12 @@ private bool HasControlOrAlt (MouseEvent me) { return ScreenToCell (clientX, clientY, out _, out _); } - /// + + /// . + /// Returns the column and row of that corresponds to a given point + /// on the screen (relative to the control client area). Returns null if the point is + /// in the header, no table is loaded or outside the control bounds. + /// /// X offset from the top left of the control. /// Y offset from the top left of the control. /// If the click is in a header this is the column clicked. @@ -1300,7 +1300,11 @@ private bool HasControlOrAlt (MouseEvent me) return ScreenToCell (clientX, clientY, out headerIfAny, out _); } - /// + /// . + /// Returns the column and row of that corresponds to a given point + /// on the screen (relative to the control client area). Returns null if the point is + /// in the header, no table is loaded or outside the control bounds. + /// /// X offset from the top left of the control. /// Y offset from the top left of the control. /// If the click is in a header this is the column clicked. diff --git a/Terminal.Gui/Views/TableView/TreeTableSource.cs b/Terminal.Gui/Views/TableView/TreeTableSource.cs index 0b06691fd0..599cc4a1e5 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -38,7 +38,7 @@ public TreeTableSource (TableView table, string firstColumnName, TreeView tre { _tableView = table; _tree = tree; - _tableView.KeyPressed += Table_KeyPress; + _tableView.KeyDown += Table_KeyPress; _tableView.MouseClick += Table_MouseClick; var colList = subsequentColumns.Keys.ToList (); @@ -68,7 +68,7 @@ public TreeTableSource (TableView table, string firstColumnName, TreeView tre /// public void Dispose () { - _tableView.KeyPressed -= Table_KeyPress; + _tableView.KeyDown -= Table_KeyPress; _tableView.MouseClick -= Table_MouseClick; _tree.Dispose (); } @@ -106,7 +106,7 @@ private string GetColumnZeroRepresentationFromTree (int row) return sb.ToString (); } - private void Table_KeyPress (object sender, KeyEventEventArgs e) + private void Table_KeyPress (object sender, Key e) { if (!IsInTreeColumn (_tableView.SelectedColumn, true)) { return; @@ -118,13 +118,13 @@ private void Table_KeyPress (object sender, KeyEventEventArgs e) return; } - if (e.KeyEvent.Key == Key.CursorLeft) { + if (e.KeyCode == KeyCode.CursorLeft) { if (_tree.IsExpanded (obj)) { _tree.Collapse (obj); e.Handled = true; } } - if (e.KeyEvent.Key == Key.CursorRight) { + if (e.KeyCode == KeyCode.CursorRight) { if (_tree.CanExpand (obj) && !_tree.IsExpanded (obj)) { _tree.Expand (obj); e.Handled = true; diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index 8f18fe9331..f1e2a43753 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -13,7 +13,6 @@ using System.Text; using Terminal.Gui.Resources; - namespace Terminal.Gui { /// /// Single-line text entry @@ -23,7 +22,7 @@ namespace Terminal.Gui { /// public class TextField : View { List _text; - int _first, _point; + int _first, _cursorPosition; int _selectedStart = -1; // -1 represents there is no text selection. string _selectedText; HistoryText _historyText = new HistoryText (); @@ -58,14 +57,12 @@ public class TextField : View { public event EventHandler TextChanging; /// - /// Changed event, raised when the text has changed. - /// + /// Changed event, raised when the text has changed. /// /// This event is raised when the changes. - /// - /// /// The passed is a containing the old value. /// + /// public event EventHandler TextChanged; /// @@ -102,8 +99,8 @@ void SetInitialProperties (string text, int w) text = ""; this._text = text.Split ("\n") [0].EnumerateRunes ().ToList (); - _point = text.GetRuneCount (); - _first = _point > w + 1 ? _point - w + 1 : 0; + _cursorPosition = text.GetRuneCount (); + _first = _cursorPosition > w + 1 ? _cursorPosition - w + 1 : 0; CanFocus = true; Used = true; WantMousePositionReports = true; @@ -115,7 +112,7 @@ void SetInitialProperties (string text, int w) // Things this view knows how to do AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); - AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; }); AddCommand (Command.LeftHomeExtend, () => { MoveHomeExtend (); return true; }); AddCommand (Command.RightEndExtend, () => { MoveEndExtend (); return true; }); AddCommand (Command.LeftHome, () => { MoveHome (); return true; }); @@ -142,102 +139,103 @@ void SetInitialProperties (string text, int w) AddCommand (Command.Paste, () => { Paste (); return true; }); AddCommand (Command.SelectAll, () => { SelectAll (); return true; }); AddCommand (Command.DeleteAll, () => { DeleteAll (); return true; }); - AddCommand (Command.Accept, () => { ShowContextMenu (); return true; }); + AddCommand (Command.ShowContextMenu, () => { ShowContextMenu (); return true; }); // Default keybindings for this view - AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); - AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); + // We follow this as closely as possible: https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts + KeyBindings.Add (KeyCode.DeleteChar, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight); - AddKeyBinding (Key.Delete, Command.DeleteCharLeft); - AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Delete, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft); - AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend); - AddKeyBinding (Key.Home | Key.ShiftMask | Key.CtrlMask, Command.LeftHomeExtend); - AddKeyBinding (Key.A | Key.ShiftMask | Key.CtrlMask, Command.LeftHomeExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask, Command.LeftHomeExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.LeftHomeExtend); + KeyBindings.Add (KeyCode.A | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.LeftHomeExtend); - AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend); - AddKeyBinding (Key.End | Key.ShiftMask | Key.CtrlMask, Command.RightEndExtend); - AddKeyBinding (Key.E | Key.ShiftMask | Key.CtrlMask, Command.RightEndExtend); + KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask, Command.RightEndExtend); + KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.RightEndExtend); + KeyBindings.Add (KeyCode.E | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.RightEndExtend); - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.Home | Key.CtrlMask, Command.LeftHome); - AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask, Command.LeftHome); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.LeftHome); - AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); - AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LeftExtend); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask, Command.LeftExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LeftExtend); - AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); - AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.RightExtend); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask, Command.RightExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.RightExtend); - AddKeyBinding (Key.CursorLeft | Key.ShiftMask | Key.CtrlMask, Command.WordLeftExtend); - AddKeyBinding (Key.CursorUp | Key.ShiftMask | Key.CtrlMask, Command.WordLeftExtend); - AddKeyBinding ((Key)((int)'B' + Key.ShiftMask | Key.AltMask), Command.WordLeftExtend); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordLeftExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordLeftExtend); + KeyBindings.Add ((KeyCode)((int)'B' + KeyCode.ShiftMask | KeyCode.AltMask), Command.WordLeftExtend); - AddKeyBinding (Key.CursorRight | Key.ShiftMask | Key.CtrlMask, Command.WordRightExtend); - AddKeyBinding (Key.CursorDown | Key.ShiftMask | Key.CtrlMask, Command.WordRightExtend); - AddKeyBinding ((Key)((int)'F' + Key.ShiftMask | Key.AltMask), Command.WordRightExtend); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordRightExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.WordRightExtend); + KeyBindings.Add ((KeyCode)((int)'F' + KeyCode.ShiftMask | KeyCode.AltMask), Command.WordRightExtend); - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left); - AddKeyBinding (Key.End, Command.RightEnd); - AddKeyBinding (Key.End | Key.CtrlMask, Command.RightEnd); - AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd); + KeyBindings.Add (KeyCode.End, Command.RightEnd); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask, Command.RightEnd); + KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.RightEnd); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right); - AddKeyBinding (Key.K | Key.CtrlMask, Command.CutToEndLine); - AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine); + KeyBindings.Add (KeyCode.K | KeyCode.CtrlMask, Command.CutToEndLine); + KeyBindings.Add (KeyCode.K | KeyCode.AltMask, Command.CutToStartLine); - AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo); - AddKeyBinding (Key.Backspace | Key.AltMask, Command.Undo); + KeyBindings.Add (KeyCode.Z | KeyCode.CtrlMask, Command.Undo); + KeyBindings.Add (KeyCode.Backspace | KeyCode.AltMask, Command.Undo); - AddKeyBinding (Key.Y | Key.CtrlMask, Command.Redo); + KeyBindings.Add (KeyCode.Y | KeyCode.CtrlMask, Command.Redo); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.WordLeft); - AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.WordLeft); - AddKeyBinding ((Key)((int)'B' + Key.AltMask), Command.WordLeft); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.WordLeft); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.WordLeft); + KeyBindings.Add ((KeyCode)((int)'B' + KeyCode.AltMask), Command.WordLeft); - AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight); - AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.WordRight); - AddKeyBinding ((Key)((int)'F' + Key.AltMask), Command.WordRight); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.WordRight); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.WordRight); + KeyBindings.Add ((KeyCode)((int)'F' + KeyCode.AltMask), Command.WordRight); - AddKeyBinding (Key.DeleteChar | Key.CtrlMask, Command.KillWordForwards); - AddKeyBinding (Key.Backspace | Key.CtrlMask, Command.KillWordBackwards); - AddKeyBinding (Key.InsertChar, Command.ToggleOverwrite); - AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy); - AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut); - AddKeyBinding (Key.V | Key.CtrlMask, Command.Paste); - AddKeyBinding (Key.T | Key.CtrlMask, Command.SelectAll); + KeyBindings.Add (KeyCode.DeleteChar | KeyCode.CtrlMask, Command.KillWordForwards); + KeyBindings.Add (KeyCode.Backspace | KeyCode.CtrlMask, Command.KillWordBackwards); + KeyBindings.Add (KeyCode.InsertChar, Command.ToggleOverwrite); + KeyBindings.Add (KeyCode.C | KeyCode.CtrlMask, Command.Copy); + KeyBindings.Add (KeyCode.X | KeyCode.CtrlMask, Command.Cut); + KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.Paste); + KeyBindings.Add (KeyCode.T | KeyCode.CtrlMask, Command.SelectAll); - AddKeyBinding (Key.R | Key.CtrlMask, Command.DeleteAll); - AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll); + KeyBindings.Add (KeyCode.R | KeyCode.CtrlMask, Command.DeleteAll); + KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.DeleteAll); _currentCulture = Thread.CurrentThread.CurrentUICulture; ContextMenu = new ContextMenu (this, BuildContextMenuBarItem ()); ContextMenu.KeyChanged += ContextMenu_KeyChanged; - AddKeyBinding (ContextMenu.Key, Command.Accept); + KeyBindings.Add (ContextMenu.Key.KeyCode, KeyBindingScope.HotKey, Command.ShowContextMenu); } private MenuBarItem BuildContextMenuBarItem () { return new MenuBarItem (new MenuItem [] { - new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, GetKeyFromCommand (Command.SelectAll)), - new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, GetKeyFromCommand (Command.DeleteAll)), - new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, GetKeyFromCommand (Command.Copy)), - new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, GetKeyFromCommand (Command.Cut)), - new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, GetKeyFromCommand (Command.Paste)), - new MenuItem (Strings.ctxUndo, "", () => Undo (), null, null, GetKeyFromCommand (Command.Undo)), - new MenuItem (Strings.ctxRedo, "", () => Redo (), null, null, GetKeyFromCommand (Command.Redo)), + new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll)), + new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll)), + new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy)), + new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut)), + new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste)), + new MenuItem (Strings.ctxUndo, "", () => Undo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo)), + new MenuItem (Strings.ctxRedo, "", () => Redo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo)), }); } private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace (e.OldKey.KeyCode, e.NewKey.KeyCode); } private void HistoryText_ChangeText (object sender, HistoryText.HistoryTextItem obj) @@ -300,8 +298,6 @@ public override Rect Frame { /// /// Sets or gets the text held by the view. /// - /// - /// public new string Text { get { return StringExtensions.ToString (_text); @@ -315,8 +311,8 @@ public override Rect Frame { var newText = OnTextChanging (value.Replace ("\t", "").Split ("\n") [0]); if (newText.Cancel) { - if (_point > _text.Count) { - _point = _text.Count; + if (_cursorPosition > _text.Count) { + _cursorPosition = _text.Count; } return; } @@ -325,8 +321,8 @@ public override Rect Frame { if (!Secret && !_historyText.IsFromHistory) { _historyText.Add (new List> () { TextModel.ToRuneCellList (oldText) }, - new Point (_point, 0)); - _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0) + new Point (_cursorPosition, 0)); + _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0) , HistoryText.LineStatus.Replaced); } @@ -334,8 +330,8 @@ public override Rect Frame { ProcessAutocomplete (); - if (_point > _text.Count) { - _point = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0); + if (_cursorPosition > _text.Count) { + _cursorPosition = Math.Max (TextModel.DisplaySize (_text, 0).size - 1, 0); } Adjust (); @@ -345,26 +341,26 @@ public override Rect Frame { /// /// Sets the secret property. - /// /// /// This makes the text entry suitable for entering passwords. /// + /// public bool Secret { get; set; } /// /// Sets or gets the current cursor position. /// public virtual int CursorPosition { - get { return _point; } + get { return _cursorPosition; } set { if (value < 0) { - _point = 0; + _cursorPosition = 0; } else if (value > _text.Count) { - _point = _text.Count; + _cursorPosition = _text.Count; } else { - _point = value; + _cursorPosition = value; } - PrepareSelection (_selectedStart, _point - _selectedStart); + PrepareSelection (_selectedStart, _cursorPosition - _selectedStart); } } @@ -399,12 +395,12 @@ public override void PositionCursor () var col = 0; for (int idx = _first < 0 ? 0 : _first; idx < _text.Count; idx++) { - if (idx == _point) + if (idx == _cursorPosition) break; var cols = _text [idx].GetColumns (); TextModel.SetCol (ref col, Frame.Width - 1, cols); } - var pos = _point - _first + Math.Min (Frame.X, 0); + var pos = _cursorPosition - _first + Math.Min (Frame.X, 0); var offB = OffSetBackground (); var containerFrame = SuperView?.BoundsToScreen (SuperView.Bounds) ?? default; var thisFrame = BoundsToScreen (Bounds); @@ -462,7 +458,7 @@ public override void OnDrawContent (Rect contentArea) for (int idx = p; idx < tcount; idx++) { var rune = _text [idx]; var cols = rune.GetColumns (); - if (idx == _point && HasFocus && !Used && _length == 0 && !ReadOnly) { + if (idx == _cursorPosition && HasFocus && !Used && _length == 0 && !ReadOnly) { Driver.SetAttribute (selColor); } else if (ReadOnly) { Driver.SetAttribute (idx >= _start && _length > 0 && idx < _start + _length ? selColor : roc); @@ -563,19 +559,20 @@ Attribute GetReadOnlyColor () void Adjust () { - if (!IsAdded) + if (!IsAdded) { return; + } int offB = OffSetBackground (); bool need = NeedsDisplay || !Used; - if (_point < _first) { - _first = _point; + if (_cursorPosition < _first) { + _first = _cursorPosition; need = true; - } else if (Frame.Width > 0 && (_first + _point - (Frame.Width + offB) == 0 || - TextModel.DisplaySize (_text, _first, _point).size >= Frame.Width + offB)) { + } else if (Frame.Width > 0 && (_first + _cursorPosition - (Frame.Width + offB) == 0 || + TextModel.DisplaySize (_text, _first, _cursorPosition).size >= Frame.Width + offB)) { _first = Math.Max (TextModel.CalculateLeftColumn (_text, _first, - _point, Frame.Width + offB), 0); + _cursorPosition, Frame.Width + offB), 0); need = true; } if (need) { @@ -617,13 +614,21 @@ void SetClipboard (IEnumerable text) Clipboard.Contents = StringExtensions.ToString (text.ToList ()); } - int _oldCursorPos; + int _preTextChangedCursorPos; + /// + public override bool? OnInvokingKeyBindings (Key a) + { + // Give autocomplete first opportunity to respond to key presses + if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (a)) { + return true; + } + return base.OnInvokingKeyBindings (a); + } + + /// TODO: Flush out these docs /// /// Processes key presses for the . - /// - /// - /// /// /// The control responds to the following keys: /// @@ -637,61 +642,56 @@ void SetClipboard (IEnumerable text) /// /// /// - public override bool ProcessKey (KeyEvent kb) + /// + /// + /// + public override bool OnProcessKeyDown (Key a) { - // remember current cursor position - // because the new calculated cursor position is needed to be set BEFORE the change event is triggest + // Remember the cursor position because the new calculated cursor position is needed + // to be set BEFORE the TextChanged event is triggered. // Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2 - _oldCursorPos = _point; - - // Give autocomplete first opportunity to respond to key presses - if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (kb)) { - return true; - } - - var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (kb), - new KeyModifiers () { Alt = kb.IsAlt, Ctrl = kb.IsCtrl, Shift = kb.IsShift })); - if (result != null) - return (bool)result; + _preTextChangedCursorPos = _cursorPosition; // Ignore other control characters. - if (kb.Key < Key.Space || kb.Key > Key.CharMask) + if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask)) { return false; + } - if (ReadOnly) + if (ReadOnly) { return true; + } - InsertText (kb); + InsertText (a, true); return true; } - void InsertText (KeyEvent kb, bool useOldCursorPos = true) + void InsertText (Key a, bool usePreTextChangedCursorPos) { - _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0)); + _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0)); List newText = _text; if (_length > 0) { newText = DeleteSelectedText (); - _oldCursorPos = _point; + _preTextChangedCursorPos = _cursorPosition; } - if (!useOldCursorPos) { - _oldCursorPos = _point; + if (!usePreTextChangedCursorPos) { + _preTextChangedCursorPos = _cursorPosition; } - var kbstr = ((Rune)(uint)kb.Key).ToString ().EnumerateRunes (); + var kbstr = a.AsRune.ToString ().EnumerateRunes (); if (Used) { - _point++; - if (_point == newText.Count + 1) { + _cursorPosition++; + if (_cursorPosition == newText.Count + 1) { SetText (newText.Concat (kbstr).ToList ()); } else { - if (_oldCursorPos > newText.Count) { - _oldCursorPos = newText.Count; + if (_preTextChangedCursorPos > newText.Count) { + _preTextChangedCursorPos = newText.Count; } - SetText (newText.GetRange (0, _oldCursorPos).Concat (kbstr).Concat (newText.GetRange (_oldCursorPos, Math.Min (newText.Count - _oldCursorPos, newText.Count)))); + SetText (newText.GetRange (0, _preTextChangedCursorPos).Concat (kbstr).Concat (newText.GetRange (_preTextChangedCursorPos, Math.Min (newText.Count - _preTextChangedCursorPos, newText.Count)))); } } else { - SetText (newText.GetRange (0, _oldCursorPos).Concat (kbstr).Concat (newText.GetRange (Math.Min (_oldCursorPos + 1, newText.Count), Math.Max (newText.Count - _oldCursorPos - 1, 0)))); - _point++; + SetText (newText.GetRange (0, _preTextChangedCursorPos).Concat (kbstr).Concat (newText.GetRange (Math.Min (_preTextChangedCursorPos + 1, newText.Count), Math.Max (newText.Count - _preTextChangedCursorPos - 1, 0)))); + _cursorPosition++; } Adjust (); } @@ -715,11 +715,11 @@ TextModel GetModel () public virtual void KillWordBackwards () { ClearAllSelection (); - var newPos = GetModel ().WordBackward (_point, 0); + var newPos = GetModel ().WordBackward (_cursorPosition, 0); if (newPos == null) return; if (newPos.Value.col != -1) { - SetText (_text.GetRange (0, newPos.Value.col).Concat (_text.GetRange (_point, _text.Count - _point))); - _point = newPos.Value.col; + SetText (_text.GetRange (0, newPos.Value.col).Concat (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition))); + _cursorPosition = newPos.Value.col; } Adjust (); } @@ -730,10 +730,10 @@ public virtual void KillWordBackwards () public virtual void KillWordForwards () { ClearAllSelection (); - var newPos = GetModel ().WordForward (_point, 0); + var newPos = GetModel ().WordForward (_cursorPosition, 0); if (newPos == null) return; if (newPos.Value.col != -1) { - SetText (_text.GetRange (0, _point).Concat (_text.GetRange (newPos.Value.col, _text.Count - newPos.Value.col))); + SetText (_text.GetRange (0, _cursorPosition).Concat (_text.GetRange (newPos.Value.col, _text.Count - newPos.Value.col))); } Adjust (); } @@ -741,20 +741,20 @@ public virtual void KillWordForwards () void MoveWordRight () { ClearAllSelection (); - var newPos = GetModel ().WordForward (_point, 0); + var newPos = GetModel ().WordForward (_cursorPosition, 0); if (newPos == null) return; if (newPos.Value.col != -1) - _point = newPos.Value.col; + _cursorPosition = newPos.Value.col; Adjust (); } void MoveWordLeft () { ClearAllSelection (); - var newPos = GetModel ().WordBackward (_point, 0); + var newPos = GetModel ().WordBackward (_cursorPosition, 0); if (newPos == null) return; if (newPos.Value.col != -1) - _point = newPos.Value.col; + _cursorPosition = newPos.Value.col; Adjust (); } @@ -803,11 +803,11 @@ void KillToStart () return; ClearAllSelection (); - if (_point == 0) + if (_cursorPosition == 0) return; - SetClipboard (_text.GetRange (0, _point)); - SetText (_text.GetRange (_point, _text.Count - _point)); - _point = 0; + SetClipboard (_text.GetRange (0, _cursorPosition)); + SetText (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition)); + _cursorPosition = 0; Adjust (); } @@ -817,19 +817,19 @@ void KillToEnd () return; ClearAllSelection (); - if (_point >= _text.Count) + if (_cursorPosition >= _text.Count) return; - SetClipboard (_text.GetRange (_point, _text.Count - _point)); - SetText (_text.GetRange (0, _point)); + SetClipboard (_text.GetRange (_cursorPosition, _text.Count - _cursorPosition)); + SetText (_text.GetRange (0, _cursorPosition)); Adjust (); } void MoveRight () { ClearAllSelection (); - if (_point == _text.Count) + if (_cursorPosition == _text.Count) return; - _point++; + _cursorPosition++; Adjust (); } @@ -839,40 +839,40 @@ void MoveRight () public void MoveEnd () { ClearAllSelection (); - _point = _text.Count; + _cursorPosition = _text.Count; Adjust (); } void MoveLeft () { ClearAllSelection (); - if (_point > 0) { - _point--; + if (_cursorPosition > 0) { + _cursorPosition--; Adjust (); } } void MoveWordRightExtend () { - if (_point < _text.Count) { - int x = _start > -1 && _start > _point ? _start : _point; + if (_cursorPosition < _text.Count) { + int x = _start > -1 && _start > _cursorPosition ? _start : _cursorPosition; var newPos = GetModel ().WordForward (x, 0); if (newPos == null) return; if (newPos.Value.col != -1) - _point = newPos.Value.col; + _cursorPosition = newPos.Value.col; PrepareSelection (x, newPos.Value.col - x); } } void MoveWordLeftExtend () { - if (_point > 0) { - int x = Math.Min (_start > -1 && _start > _point ? _start : _point, _text.Count); + if (_cursorPosition > 0) { + int x = Math.Min (_start > -1 && _start > _cursorPosition ? _start : _cursorPosition, _text.Count); if (x > 0) { var newPos = GetModel ().WordBackward (x, 0); if (newPos == null) return; if (newPos.Value.col != -1) - _point = newPos.Value.col; + _cursorPosition = newPos.Value.col; PrepareSelection (x, newPos.Value.col - x); } } @@ -880,65 +880,68 @@ void MoveWordLeftExtend () void MoveRightExtend () { - if (_point < _text.Count) { - PrepareSelection (_point++, 1); + if (_cursorPosition < _text.Count) { + PrepareSelection (_cursorPosition++, 1); } } void MoveLeftExtend () { - if (_point > 0) { - PrepareSelection (_point--, -1); + if (_cursorPosition > 0) { + PrepareSelection (_cursorPosition--, -1); } } void MoveHome () { ClearAllSelection (); - _point = 0; + _cursorPosition = 0; Adjust (); } void MoveEndExtend () { - if (_point <= _text.Count) { - int x = _point; - _point = _text.Count; - PrepareSelection (x, _point - x); + if (_cursorPosition <= _text.Count) { + int x = _cursorPosition; + _cursorPosition = _text.Count; + PrepareSelection (x, _cursorPosition - x); } } void MoveHomeExtend () { - if (_point > 0) { - int x = _point; - _point = 0; - PrepareSelection (x, _point - x); + if (_cursorPosition > 0) { + int x = _cursorPosition; + _cursorPosition = 0; + PrepareSelection (x, _cursorPosition - x); } } /// - /// Deletes the left character. + /// Deletes the character to the left. /// - public virtual void DeleteCharLeft (bool useOldCursorPos = true) + /// If set to true use the cursor position cached + /// ; otherwise use . + /// use . + public virtual void DeleteCharLeft (bool usePreTextChangedCursorPos) { if (ReadOnly) return; - _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0)); + _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0)); if (_length == 0) { - if (_point == 0) + if (_cursorPosition == 0) return; - if (!useOldCursorPos) { - _oldCursorPos = _point; + if (!usePreTextChangedCursorPos) { + _preTextChangedCursorPos = _cursorPosition; } - _point--; - if (_oldCursorPos < _text.Count) { - SetText (_text.GetRange (0, _oldCursorPos - 1).Concat (_text.GetRange (_oldCursorPos, _text.Count - _oldCursorPos))); + _cursorPosition--; + if (_preTextChangedCursorPos < _text.Count) { + SetText (_text.GetRange (0, _preTextChangedCursorPos - 1).Concat (_text.GetRange (_preTextChangedCursorPos, _text.Count - _preTextChangedCursorPos))); } else { - SetText (_text.GetRange (0, _oldCursorPos - 1)); + SetText (_text.GetRange (0, _preTextChangedCursorPos - 1)); } Adjust (); } else { @@ -949,20 +952,20 @@ public virtual void DeleteCharLeft (bool useOldCursorPos = true) } /// - /// Deletes the right character. + /// Deletes the character to the right. /// public virtual void DeleteCharRight () { if (ReadOnly) return; - _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_point, 0)); + _historyText.Add (new List> () { TextModel.ToRuneCells (_text) }, new Point (_cursorPosition, 0)); if (_length == 0) { - if (_text.Count == 0 || _text.Count == _point) + if (_text.Count == 0 || _text.Count == _cursorPosition) return; - SetText (_text.GetRange (0, _point).Concat (_text.GetRange (_point + 1, _text.Count - (_point + 1)))); + SetText (_text.GetRange (0, _cursorPosition).Concat (_text.GetRange (_cursorPosition + 1, _text.Count - (_cursorPosition + 1)))); Adjust (); } else { var newText = DeleteSelectedText (); @@ -1007,7 +1010,7 @@ public void DeleteAll () _selectedStart = 0; MoveEndExtend (); - DeleteCharLeft (); + DeleteCharLeft (false); SetNeedsDisplay (); } @@ -1024,7 +1027,7 @@ public int SelectedStart { } else { _selectedStart = value; } - PrepareSelection (_selectedStart, _point - _selectedStart); + PrepareSelection (_selectedStart, _cursorPosition - _selectedStart); } } @@ -1105,7 +1108,7 @@ public override bool MouseEvent (MouseEvent ev) if (newPosFw == null) return true; ClearAllSelection (); if (newPosFw.Value.col != -1 && sbw != -1) { - _point = newPosFw.Value.col; + _cursorPosition = newPosFw.Value.col; } PrepareSelection (sbw, newPosFw.Value.col - sbw); } else if (ev.Flags == MouseFlags.Button1TripleClicked) { @@ -1148,14 +1151,14 @@ int PositionCursor (int x, bool getX = true) pX = TextModel.GetColFromX (_text, _first, x); } if (_first + pX > _text.Count) { - _point = _text.Count; + _cursorPosition = _text.Count; } else if (_first + pX < _first) { - _point = 0; + _cursorPosition = 0; } else { - _point = _first + pX; + _cursorPosition = _first + pX; } - return _point; + return _cursorPosition; } void PrepareSelection (int x, int direction = 0) @@ -1174,6 +1177,7 @@ void PrepareSelection (int x, int direction = 0) } else if (_start > -1 && _length == 0) { _selectedText = null; } + SetNeedsDisplay (); } else if (_length > 0 || _selectedText != null) { ClearAllSelection (); } @@ -1199,8 +1203,8 @@ public void ClearAllSelection () void SetSelectedStartSelectedLength () { - if (SelectedStart > -1 && _point < SelectedStart) { - _start = _point; + if (SelectedStart > -1 && _cursorPosition < SelectedStart) { + _start = _cursorPosition; } else { _start = SelectedStart; } @@ -1234,12 +1238,12 @@ public virtual void Cut () List DeleteSelectedText () { SetSelectedStartSelectedLength (); - int selStart = SelectedStart > -1 ? _start : _point; + int selStart = SelectedStart > -1 ? _start : _cursorPosition; var newText = StringExtensions.ToString (_text.GetRange (0, selStart)) + StringExtensions.ToString (_text.GetRange (selStart + _length, _text.Count - (selStart + _length))); ClearAllSelection (); - _point = selStart >= newText.GetRuneCount () ? newText.GetRuneCount () : selStart; + _cursorPosition = selStart >= newText.GetRuneCount () ? newText.GetRuneCount () : selStart; return newText.ToRuneList (); } @@ -1259,7 +1263,7 @@ public virtual void Paste () cbTxt + StringExtensions.ToString (_text.GetRange (selStart + _length, _text.Count - (selStart + _length))); - _point = selStart + cbTxt.GetRuneCount (); + _cursorPosition = selStart + cbTxt.GetRuneCount (); ClearAllSelection (); SetNeedsDisplay (); Adjust (); @@ -1298,21 +1302,21 @@ public CursorVisibility DesiredCursorVisibility { /// exactly as if the user had just typed it /// /// Text to add - /// If uses the . + /// Use the previous cursor position. public void InsertText (string toAdd, bool useOldCursorPos = true) { foreach (var ch in toAdd) { - Key key; + KeyCode key; try { - key = (Key)ch; + key = (KeyCode)ch; } catch (Exception) { throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key"); } - InsertText (new KeyEvent () { Key = key }, useOldCursorPos); + InsertText (new Key () { KeyCode = key }, useOldCursorPos); } } diff --git a/Terminal.Gui/Views/TextValidateField.cs b/Terminal.Gui/Views/TextValidateField.cs index f687a82a5d..7f3f74d349 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -400,15 +400,15 @@ void Initialize () AddCommand (Command.Right, () => { CursorRight (); return true; }); // Default keybindings for this view - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.End, Command.RightEnd); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.End, Command.RightEnd); - AddKeyBinding (Key.Delete, Command.DeleteCharRight); - AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.DeleteChar, Command.DeleteCharRight); - AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); } /// @@ -612,20 +612,17 @@ bool EndKeyHandler () } /// - public override bool ProcessKey (KeyEvent kb) + public override bool OnProcessKeyDown (Key a) { if (provider == null) { return false; } - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - if (kb.Key < Key.Space || kb.Key > Key.CharMask) + if (a.AsRune == default) { return false; - - var key = new Rune ((uint)kb.KeyValue); + } + + var key = a.AsRune; var inserted = provider.InsertAt ((char)key.Value, cursorPosition); @@ -633,7 +630,7 @@ public override bool ProcessKey (KeyEvent kb) CursorRight (); } - return true; + return false; } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index cca066948b..70205045de 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1673,126 +1673,127 @@ void SetInitialProperties () }); // Default keybindings for this view - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.V | KeyCode.CtrlMask, Command.PageDown); - AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); + KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend); - AddKeyBinding (Key.PageUp, Command.PageUp); - AddKeyBinding (((int)'V' + Key.AltMask), Command.PageUp); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add (((int)'V' + KeyCode.AltMask), Command.PageUp); - AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); + KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend); - AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); - AddKeyBinding (Key.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.N | KeyCode.CtrlMask, Command.LineDown); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); - AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend); - AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); - AddKeyBinding (Key.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.P | KeyCode.CtrlMask, Command.LineUp); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend); - AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); - AddKeyBinding (Key.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); - AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.ShiftMask, Command.RightExtend); - AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); - AddKeyBinding (Key.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.ShiftMask, Command.LeftExtend); - AddKeyBinding (Key.Delete, Command.DeleteCharLeft); - AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Delete, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft); - AddKeyBinding (Key.Home, Command.StartOfLine); - AddKeyBinding (Key.A | Key.CtrlMask, Command.StartOfLine); + KeyBindings.Add (KeyCode.Home, Command.StartOfLine); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.StartOfLine); - AddKeyBinding (Key.Home | Key.ShiftMask, Command.StartOfLineExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.ShiftMask, Command.StartOfLineExtend); - AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); - AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.DeleteChar, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight); - AddKeyBinding (Key.End, Command.EndOfLine); - AddKeyBinding (Key.E | Key.CtrlMask, Command.EndOfLine); + KeyBindings.Add (KeyCode.End, Command.EndOfLine); + KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.EndOfLine); - AddKeyBinding (Key.End | Key.ShiftMask, Command.EndOfLineExtend); + KeyBindings.Add (KeyCode.End | KeyCode.ShiftMask, Command.EndOfLineExtend); - AddKeyBinding (Key.K | Key.CtrlMask, Command.CutToEndLine); // kill-to-end - AddKeyBinding (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, Command.CutToEndLine); // kill-to-end + KeyBindings.Add (KeyCode.K | KeyCode.CtrlMask, Command.CutToEndLine); // kill-to-end + KeyBindings.Add (KeyCode.DeleteChar | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.CutToEndLine); // kill-to-end - AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine); // kill-to-start - AddKeyBinding (Key.Backspace | Key.CtrlMask | Key.ShiftMask, Command.CutToStartLine); // kill-to-start + KeyBindings.Add (KeyCode.K | KeyCode.AltMask, Command.CutToStartLine); // kill-to-start + KeyBindings.Add (KeyCode.Backspace | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.CutToStartLine); // kill-to-start - AddKeyBinding (Key.Y | Key.CtrlMask, Command.Paste); // Control-y, yank - AddKeyBinding (Key.Space | Key.CtrlMask, Command.ToggleExtend); + KeyBindings.Add (KeyCode.Y | KeyCode.CtrlMask, Command.Paste); // Control-y, yank + KeyBindings.Add (KeyCode.Space | KeyCode.CtrlMask, Command.ToggleExtend); - AddKeyBinding (((int)'C' + Key.AltMask), Command.Copy); - AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy); + KeyBindings.Add (((int)'C' + KeyCode.AltMask), Command.Copy); + KeyBindings.Add (KeyCode.C | KeyCode.CtrlMask, Command.Copy); - AddKeyBinding (((int)'W' + Key.AltMask), Command.Cut); - AddKeyBinding (Key.W | Key.CtrlMask, Command.Cut); - AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut); + KeyBindings.Add (((int)'W' + KeyCode.AltMask), Command.Cut); + KeyBindings.Add (KeyCode.W | KeyCode.CtrlMask, Command.Cut); + KeyBindings.Add (KeyCode.X | KeyCode.CtrlMask, Command.Cut); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.WordLeft); - AddKeyBinding ((Key)((int)'B' + Key.AltMask), Command.WordLeft); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.WordLeft); + KeyBindings.Add ((KeyCode)((int)'B' + KeyCode.AltMask), Command.WordLeft); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask | Key.ShiftMask, Command.WordLeftExtend); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.WordLeftExtend); - AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight); - AddKeyBinding ((Key)((int)'F' + Key.AltMask), Command.WordRight); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.WordRight); + KeyBindings.Add ((KeyCode)((int)'F' + KeyCode.AltMask), Command.WordRight); - AddKeyBinding (Key.CursorRight | Key.CtrlMask | Key.ShiftMask, Command.WordRightExtend); - AddKeyBinding (Key.DeleteChar | Key.CtrlMask, Command.KillWordForwards); // kill-word-forwards - AddKeyBinding (Key.Backspace | Key.CtrlMask, Command.KillWordBackwards); // kill-word-backwards + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.WordRightExtend); + KeyBindings.Add (KeyCode.DeleteChar | KeyCode.CtrlMask, Command.KillWordForwards); // kill-word-forwards + KeyBindings.Add (KeyCode.Backspace | KeyCode.CtrlMask, Command.KillWordBackwards); // kill-word-backwards - AddKeyBinding (Key.Enter, Command.NewLine); - AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd); - AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend); - AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome); - AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend); - AddKeyBinding (Key.T | Key.CtrlMask, Command.SelectAll); - AddKeyBinding (Key.InsertChar, Command.ToggleOverwrite); - AddKeyBinding (Key.Tab, Command.Tab); - AddKeyBinding (Key.BackTab | Key.ShiftMask, Command.BackTab); + // BUGBUG: If AllowsReturn is false, Key.Enter should not be bound (so that Toplevel can cause Command.Accept). + KeyBindings.Add (KeyCode.Enter, Command.NewLine); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask, Command.BottomEnd); + KeyBindings.Add (KeyCode.End | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.BottomEndExtend); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask, Command.TopHome); + KeyBindings.Add (KeyCode.Home | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.TopHomeExtend); + KeyBindings.Add (KeyCode.T | KeyCode.CtrlMask, Command.SelectAll); + KeyBindings.Add (KeyCode.InsertChar, Command.ToggleOverwrite); + KeyBindings.Add (KeyCode.Tab, Command.Tab); + KeyBindings.Add (KeyCode.Tab | KeyCode.ShiftMask, Command.BackTab); - AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextView); - AddKeyBinding (Application.AlternateForwardKey, Command.NextView); + KeyBindings.Add (KeyCode.Tab | KeyCode.CtrlMask, Command.NextView); + KeyBindings.Add ((KeyCode)Application.AlternateForwardKey, Command.NextView); - AddKeyBinding (Key.Tab | Key.CtrlMask | Key.ShiftMask, Command.PreviousView); - AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousView); + KeyBindings.Add (KeyCode.Tab | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.PreviousView); + KeyBindings.Add ((KeyCode)Application.AlternateBackwardKey, Command.PreviousView); - AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo); - AddKeyBinding (Key.R | Key.CtrlMask, Command.Redo); + KeyBindings.Add (KeyCode.Z | KeyCode.CtrlMask, Command.Undo); + KeyBindings.Add (KeyCode.R | KeyCode.CtrlMask, Command.Redo); - AddKeyBinding (Key.G | Key.CtrlMask, Command.DeleteAll); - AddKeyBinding (Key.D | Key.CtrlMask | Key.ShiftMask, Command.DeleteAll); + KeyBindings.Add (KeyCode.G | KeyCode.CtrlMask, Command.DeleteAll); + KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask | KeyCode.ShiftMask, Command.DeleteAll); _currentCulture = Thread.CurrentThread.CurrentUICulture; ContextMenu = new ContextMenu () { MenuItems = BuildContextMenuBarItem () }; ContextMenu.KeyChanged += ContextMenu_KeyChanged!; - AddKeyBinding (ContextMenu.Key, Command.Accept); + KeyBindings.Add ((KeyCode)ContextMenu.Key, KeyBindingScope.HotKey, Command.Accept); } private MenuBarItem BuildContextMenuBarItem () { return new MenuBarItem (new MenuItem [] { - new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, GetKeyFromCommand (Command.SelectAll)), - new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, GetKeyFromCommand (Command.DeleteAll)), - new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, GetKeyFromCommand (Command.Copy)), - new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, GetKeyFromCommand (Command.Cut)), - new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, GetKeyFromCommand (Command.Paste)), - new MenuItem (Strings.ctxUndo, "", () => Undo (), null, null, GetKeyFromCommand (Command.Undo)), - new MenuItem (Strings.ctxRedo, "", () => Redo (), null, null, GetKeyFromCommand (Command.Redo)), + new MenuItem (Strings.ctxSelectAll, "", () => SelectAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.SelectAll)), + new MenuItem (Strings.ctxDeleteAll, "", () => DeleteAll (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.DeleteAll)), + new MenuItem (Strings.ctxCopy, "", () => Copy (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Copy)), + new MenuItem (Strings.ctxCut, "", () => Cut (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Cut)), + new MenuItem (Strings.ctxPaste, "", () => Paste (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Paste)), + new MenuItem (Strings.ctxUndo, "", () => Undo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Undo)), + new MenuItem (Strings.ctxRedo, "", () => Redo (), null, null, (KeyCode)KeyBindings.GetKeyFromCommands (Command.Redo)), }); } private void ContextMenu_KeyChanged (object sender, KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); } private void Model_LinesLoaded (object sender, EventArgs e) @@ -1866,12 +1867,12 @@ void TextView_Initialized (object sender, EventArgs e) void Top_AlternateBackwardKeyChanged (object sender, KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); } void Top_AlternateForwardKeyChanged (object sender, KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); } /// @@ -2516,9 +2517,9 @@ private static void SetValidUsedColor (ColorScheme colorScheme) { // BUGBUG: (v2 truecolor) This code depends on 8-bit color names; disabling for now //if ((colorScheme!.HotNormal.Foreground & colorScheme.Focus.Background) == colorScheme.Focus.Foreground) { - Driver.SetAttribute (new Attribute (colorScheme.Focus.Background, colorScheme.Focus.Foreground)); + Driver.SetAttribute (new Attribute (colorScheme.Focus.Background, colorScheme.Focus.Foreground)); //} else { - //Driver.SetAttribute (new Attribute (colorScheme!.HotNormal.Foreground & colorScheme.Focus.Background, colorScheme.Focus.Foreground)); + //Driver.SetAttribute (new Attribute (colorScheme!.HotNormal.Foreground & colorScheme.Focus.Background, colorScheme.Focus.Foreground)); //} } @@ -2882,8 +2883,8 @@ void UpdateWrapModel ([CallerMemberName] string? caller = null) _selectionStartColumn = nStartCol; _wrapNeeded = true; - SetNeedsDisplay(); - } + SetNeedsDisplay (); + } if (_currentCaller != null) throw new InvalidOperationException ($"WordWrap settings was changed after the {_currentCaller} call."); } @@ -3058,16 +3059,16 @@ public void InsertText (string toAdd) { foreach (var ch in toAdd) { - Key key; + KeyCode key; try { - key = (Key)ch; + key = (KeyCode)ch; } catch (Exception) { throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key"); } - InsertText (new KeyEvent () { Key = key }); + InsertText (new Key () { KeyCode = key }); } if (NeedsDisplay) { @@ -3403,28 +3404,32 @@ public void ScrollTo (int idx, bool isRow = true) bool _shiftSelecting; /// - public override bool ProcessKey (KeyEvent kb) + public override bool? OnInvokingKeyBindings (Key a) { - if (!CanFocus) { + // Give autocomplete first opportunity to respond to key presses + if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (a)) { return true; } + return base.OnInvokingKeyBindings (a); + } - // Give autocomplete first opportunity to respond to key presses - if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (kb)) { + /// + public override bool OnProcessKeyDown (Key a) + { + if (!CanFocus) { return true; } - var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (kb), - new KeyModifiers () { Alt = kb.IsAlt, Ctrl = kb.IsCtrl, Shift = kb.IsShift })); - if (result != null) - return (bool)result; + ResetColumnTrack (); + // Ignore control characters and other special keys - if (kb.Key < Key.Space || kb.Key > Key.CharMask) + if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask)) { return false; + } - InsertText (kb); + InsertText (a); DoNeededAction (); return true; @@ -3793,7 +3798,7 @@ bool ProcessTab () if (!AllowsTab || _isReadOnly) { return ProcessMoveNextView (); } - InsertText (new KeyEvent ((Key)'\t', null)); + InsertText (new Key ((KeyCode)'\t')); DoNeededAction (); return true; } @@ -4320,11 +4325,12 @@ void ResetAllTrack () _continuousFind = false; } - bool InsertText (KeyEvent kb, ColorScheme? colorScheme = null) + bool InsertText (Key a, ColorScheme? colorScheme = null) { //So that special keys like tab can be processed - if (_isReadOnly) + if (_isReadOnly) { return true; + } SetWrapModel (); @@ -4333,22 +4339,22 @@ bool InsertText (KeyEvent kb, ColorScheme? colorScheme = null) if (_selecting) { ClearSelectedRegion (); } - if (kb.Key == Key.Enter) { + if (a.KeyCode == KeyCode.Enter) { _model.AddLine (_currentRow + 1, new List ()); _currentRow++; _currentColumn = 0; - } else if ((uint)kb.Key == 13) { + } else if ((uint)a.KeyCode == '\r') { _currentColumn = 0; } else { if (Used) { - Insert (new RuneCell { Rune = (Rune)(uint)kb.Key, ColorScheme = colorScheme }); + Insert (new RuneCell { Rune = a.AsRune, ColorScheme = colorScheme }); _currentColumn++; if (_currentColumn >= _leftColumn + Frame.Width) { _leftColumn++; SetNeedsDisplay (); } } else { - Insert (new RuneCell { Rune = (Rune)(uint)kb.Key, ColorScheme = colorScheme }); + Insert (new RuneCell { Rune = a.AsRune, ColorScheme = colorScheme }); _currentColumn++; } } @@ -4390,14 +4396,14 @@ public void DeleteAll () } /// - public override bool OnKeyUp (KeyEvent kb) + public override bool OnKeyUp (Key a) { - switch (kb.Key) { - case Key.Space | Key.CtrlMask: + switch (a.KeyCode) { + case KeyCode.Space | KeyCode.CtrlMask: return true; } - return false; + return base.OnKeyUp (a); } void DoNeededAction () diff --git a/Terminal.Gui/Views/TileView.cs b/Terminal.Gui/Views/TileView.cs index fcdc3d2a16..e32eef6c8b 100644 --- a/Terminal.Gui/Views/TileView.cs +++ b/Terminal.Gui/Views/TileView.cs @@ -11,29 +11,30 @@ namespace Terminal.Gui { /// public class TileView : View { TileView parentTileView; - + + // TODO: Update to use Key instead of KeyCode /// /// The keyboard key that the user can press to toggle resizing /// of splitter lines. Mouse drag splitting is always enabled. /// - public Key ToggleResizable { get; set; } = Key.CtrlMask | Key.F10; + public KeyCode ToggleResizable { get; set; } = KeyCode.CtrlMask | KeyCode.F10; - List tiles; - private List splitterDistances; - private List splitterLines; + List _tiles; + private List _splitterDistances; + private List _splitterLines; /// /// The sub sections hosted by the view /// - public IReadOnlyCollection Tiles => tiles.AsReadOnly (); + public IReadOnlyCollection Tiles => _tiles.AsReadOnly (); /// /// The splitter locations. Note that there will be N-1 splitters where /// N is the number of . /// - public IReadOnlyCollection SplitterDistances => splitterDistances.AsReadOnly (); + public IReadOnlyCollection SplitterDistances => _splitterDistances.AsReadOnly (); - private Orientation orientation = Orientation.Vertical; + private Orientation _orientation = Orientation.Vertical; /// /// Creates a new instance of the class with @@ -63,7 +64,7 @@ public TileView (int tiles) /// protected virtual void OnSplitterMoved (int idx) { - SplitterMoved?.Invoke (this, new SplitterEventArgs (this, idx, splitterDistances [idx])); + SplitterMoved?.Invoke (this, new SplitterEventArgs (this, idx, _splitterDistances [idx])); } /// @@ -73,22 +74,22 @@ protected virtual void OnSplitterMoved (int idx) /// public void RebuildForTileCount (int count) { - tiles = new List (); - splitterDistances = new List (); - if (splitterLines != null) { - foreach (var sl in splitterLines) { + _tiles = new List (); + _splitterDistances = new List (); + if (_splitterLines != null) { + foreach (var sl in _splitterLines) { sl.Dispose (); } } - splitterLines = new List (); + _splitterLines = new List (); RemoveAll (); - foreach (var tile in tiles) { + foreach (var tile in _tiles) { tile.ContentView.Dispose (); tile.ContentView = null; } - tiles.Clear (); - splitterDistances.Clear (); + _tiles.Clear (); + _splitterDistances.Clear (); if (count == 0) { return; @@ -97,14 +98,14 @@ public void RebuildForTileCount (int count) for (int i = 0; i < count; i++) { if (i > 0) { var currentPos = Pos.Percent ((100 / count) * i); - splitterDistances.Add (currentPos); + _splitterDistances.Add (currentPos); var line = new TileViewLineView (this, i - 1); Add (line); - splitterLines.Add (line); + _splitterLines.Add (line); } var tile = new Tile (); - tiles.Add (tile); + _tiles.Add (tile); Add (tile.ContentView); tile.TitleChanged += (s, e) => SetNeedsDisplay (); } @@ -126,21 +127,21 @@ public Tile InsertTile (int idx) Tile toReturn = null; - for (int i = 0; i < tiles.Count; i++) { + for (int i = 0; i < _tiles.Count; i++) { if (i != idx) { var oldTile = oldTiles [i > idx ? i - 1 : i]; // remove the new empty View - Remove (tiles [i].ContentView); - tiles [i].ContentView.Dispose (); - tiles [i].ContentView = null; + Remove (_tiles [i].ContentView); + _tiles [i].ContentView.Dispose (); + _tiles [i].ContentView = null; // restore old Tile and View - tiles [i] = oldTile; - Add (tiles [i].ContentView); + _tiles [i] = oldTile; + Add (_tiles [i].ContentView); } else { - toReturn = tiles [i]; + toReturn = _tiles [i]; } } SetNeedsDisplay (); @@ -169,19 +170,19 @@ public Tile RemoveTile (int idx) RebuildForTileCount (oldTiles.Length - 1); - for (int i = 0; i < tiles.Count; i++) { + for (int i = 0; i < _tiles.Count; i++) { int oldIdx = i >= idx ? i + 1 : i; var oldTile = oldTiles [oldIdx]; // remove the new empty View - Remove (tiles [i].ContentView); - tiles [i].ContentView.Dispose (); - tiles [i].ContentView = null; + Remove (_tiles [i].ContentView); + _tiles [i].ContentView.Dispose (); + _tiles [i].ContentView = null; // restore old Tile and View - tiles [i] = oldTile; - Add (tiles [i].ContentView); + _tiles [i] = oldTile; + Add (_tiles [i].ContentView); } SetNeedsDisplay (); @@ -196,8 +197,8 @@ public Tile RemoveTile (int idx) /// public int IndexOf (View toFind, bool recursive = false) { - for (int i = 0; i < tiles.Count; i++) { - var v = tiles [i].ContentView; + for (int i = 0; i < _tiles.Count; i++) { + var v = _tiles [i].ContentView; if (v == toFind) { return i; @@ -236,9 +237,9 @@ private bool RecursiveContains (IEnumerable haystack, View needle) /// Orientation of the dividing line (Horizontal or Vertical). /// public Orientation Orientation { - get { return orientation; } + get { return _orientation; } set { - orientation = value; + _orientation = value; if (IsInitialized) { LayoutSubviews (); } @@ -266,7 +267,7 @@ public override void LayoutSubviews () } /// - /// Attempts to update the of line at + /// Attempts to update the of line at /// to the new . Returns false if the new position is not allowed because of /// , location of other splitters etc. /// @@ -279,13 +280,13 @@ public bool SetSplitterPos (int idx, Pos value) throw new ArgumentException ($"Only Percent and Absolute values are supported. Passed value was {value.GetType ().Name}"); } - var fullSpace = orientation == Orientation.Vertical ? Bounds.Width : Bounds.Height; + var fullSpace = _orientation == Orientation.Vertical ? Bounds.Width : Bounds.Height; if (fullSpace != 0 && !IsValidNewSplitterPos (idx, value, fullSpace)) { return false; } - splitterDistances [idx] = value; + _splitterDistances [idx] = value; GetRootTileView ().LayoutSubviews (); OnSplitterMoved (idx); return true; @@ -329,7 +330,7 @@ public override void OnDrawContent (Rect contentArea) } foreach (var line in allLines) { - bool isRoot = splitterLines.Contains (line); + bool isRoot = _splitterLines.Contains (line); line.BoundsToScreen (0, 0, out var x1, out var y1); var origin = ScreenToFrame (x1, y1); @@ -399,7 +400,7 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) { // when splitting a view into 2 sub views we will need to migrate // the title too - var tile = tiles [idx]; + var tile = _tiles [idx]; var title = tile.Title; View toMove = tile.ContentView; @@ -428,27 +429,28 @@ public bool TrySplitTile (int idx, int numberOfPanels, out TileView result) tile.ContentView = newContainer; - var newTileView1 = newContainer.tiles [0].ContentView; + var newTileView1 = newContainer._tiles [0].ContentView; // Add the original content into the first view of the new container foreach (var childView in childViews) { newTileView1.Add (childView); } // Move the title across too - newContainer.tiles [0].Title = title; + newContainer._tiles [0].Title = title; tile.Title = string.Empty; result = newContainer; return true; } + //// BUGBUG: Why is this not handled by a key binding??? /// - public override bool ProcessHotKey (KeyEvent keyEvent) + public override bool OnProcessKeyDown (Key keyEvent) { bool focusMoved = false; - if (keyEvent.Key == ToggleResizable) { - foreach (var l in splitterLines) { + if (keyEvent.KeyCode == ToggleResizable) { + foreach (var l in _splitterLines) { var iniBefore = l.IsInitialized; l.IsInitialized = false; @@ -463,13 +465,13 @@ public override bool ProcessHotKey (KeyEvent keyEvent) return true; } - return base.ProcessHotKey (keyEvent); + return false; } private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) { int newSize = value.Anchor (fullSpace); - bool isGettingBigger = newSize > splitterDistances [idx].Anchor (fullSpace); + bool isGettingBigger = newSize > _splitterDistances [idx].Anchor (fullSpace); int lastSplitterOrBorder = HasBorder () ? 1 : 0; int nextSplitterOrBorder = HasBorder () ? fullSpace - 1 : fullSpace; @@ -491,7 +493,7 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) // Do not allow splitter to move left of the one before if (idx > 0) { - int posLeft = splitterDistances [idx - 1].Anchor (fullSpace); + int posLeft = _splitterDistances [idx - 1].Anchor (fullSpace); if (newSize <= posLeft) { return false; @@ -501,8 +503,8 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) } // Do not allow splitter to move right of the one after - if (idx + 1 < splitterDistances.Count) { - int posRight = splitterDistances [idx + 1].Anchor (fullSpace); + if (idx + 1 < _splitterDistances.Count) { + int posRight = _splitterDistances [idx + 1].Anchor (fullSpace); if (newSize >= posRight) { return false; @@ -519,7 +521,7 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) } // don't grow if it would take us below min size of right panel - if (spaceForNext < tiles [idx + 1].MinSize) { + if (spaceForNext < _tiles [idx + 1].MinSize) { return false; } } else { @@ -531,7 +533,7 @@ private bool IsValidNewSplitterPos (int idx, Pos value, int fullSpace) } // don't shrink if it would take us below min size of left panel - if (spaceForLast < tiles [idx].MinSize) { + if (spaceForLast < _tiles [idx].MinSize) { return false; } } @@ -629,22 +631,22 @@ private void Setup (Rect contentArea) return; } - for (int i = 0; i < splitterLines.Count; i++) { - var line = splitterLines [i]; + for (int i = 0; i < _splitterLines.Count; i++) { + var line = _splitterLines [i]; line.Orientation = Orientation; - line.Width = orientation == Orientation.Vertical + line.Width = _orientation == Orientation.Vertical ? 1 : Dim.Fill (); - line.Height = orientation == Orientation.Vertical + line.Height = _orientation == Orientation.Vertical ? Dim.Fill () : 1; - line.LineRune = orientation == Orientation.Vertical ? + line.LineRune = _orientation == Orientation.Vertical ? CM.Glyphs.VLine : CM.Glyphs.HLine; - if (orientation == Orientation.Vertical) { - line.X = splitterDistances [i]; + if (_orientation == Orientation.Vertical) { + line.X = _splitterDistances [i]; line.Y = 0; } else { - line.Y = splitterDistances [i]; + line.Y = _splitterDistances [i]; line.X = 0; } @@ -652,8 +654,8 @@ private void Setup (Rect contentArea) HideSplittersBasedOnTileVisibility (); - var visibleTiles = tiles.Where (t => t.ContentView.Visible).ToArray (); - var visibleSplitterLines = splitterLines.Where (l => l.Visible).ToArray (); + var visibleTiles = _tiles.Where (t => t.ContentView.Visible).ToArray (); + var visibleSplitterLines = _splitterLines.Where (l => l.Visible).ToArray (); for (int i = 0; i < visibleTiles.Length; i++) { var tile = visibleTiles [i]; @@ -674,20 +676,20 @@ private void Setup (Rect contentArea) private void HideSplittersBasedOnTileVisibility () { - if (splitterLines.Count == 0) { + if (_splitterLines.Count == 0) { return; } - foreach (var line in splitterLines) { + foreach (var line in _splitterLines) { line.Visible = true; } - for (int i = 0; i < tiles.Count; i++) { - if (!tiles [i].ContentView.Visible) { + for (int i = 0; i < _tiles.Count; i++) { + if (!_tiles [i].ContentView.Visible) { // when a tile is not visible, prefer hiding // the splitter on it's left - var candidate = splitterLines [Math.Max (0, i - 1)]; + var candidate = _splitterLines [Math.Max (0, i - 1)]; // unless that splitter is already hidden // e.g. when hiding panels 0 and 1 of a 3 panel @@ -695,7 +697,7 @@ private void HideSplittersBasedOnTileVisibility () if (candidate.Visible) { candidate.Visible = false; } else { - splitterLines [Math.Min (i, splitterLines.Count - 1)].Visible = false; + _splitterLines [Math.Min (i, _splitterLines.Count - 1)].Visible = false; } } @@ -799,23 +801,10 @@ public TileViewLineView (TileView parent, int idx) return MoveSplitter (0, 1); }); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorDown, Command.LineDown); - } - - public override bool ProcessKey (KeyEvent kb) - { - if (!CanFocus || !HasFocus) { - return base.ProcessKey (kb); - } - - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - return base.ProcessKey (kb); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); } public override void PositionCursor () diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index ff0b7198b8..173edd2ffb 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -80,30 +80,30 @@ void Initialize (TimeSpan time, bool isShort = false) // Things this view knows how to do AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); - AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; }); AddCommand (Command.LeftHome, () => MoveHome ()); AddCommand (Command.Left, () => MoveLeft ()); AddCommand (Command.RightEnd, () => MoveEnd ()); AddCommand (Command.Right, () => MoveRight ()); // Default keybindings for this view - AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); - AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.DeleteChar, Command.DeleteCharRight); + KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight); - AddKeyBinding (Key.Delete, Command.DeleteCharLeft); - AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Delete, Command.DeleteCharLeft); + KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft); - AddKeyBinding (Key.Home, Command.LeftHome); - AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome); + KeyBindings.Add (KeyCode.Home, Command.LeftHome); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.LeftHome); - AddKeyBinding (Key.CursorLeft, Command.Left); - AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left); - AddKeyBinding (Key.End, Command.RightEnd); - AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd); + KeyBindings.Add (KeyCode.End, Command.RightEnd); + KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.RightEnd); - AddKeyBinding (Key.CursorRight, Command.Right); - AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right); } void TextField_TextChanged (object sender, TextChangedEventArgs e) @@ -247,23 +247,23 @@ void AdjCursorPosition () } /// - public override bool ProcessKey (KeyEvent kb) + public override bool OnProcessKeyDown (Key a) { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - // Ignore non-numeric characters. - if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9)) - return false; - - if (ReadOnly) + if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9) { + if (!ReadOnly) { + if (SetText ((Rune)a)) { + IncCursorPosition (); + } + } return true; + } - if (SetText (((Rune)(uint)kb.Key).ToString ().EnumerateRunes ().First ())) - IncCursorPosition (); - - return true; + if (a.IsKeyCodeAtoZ) { + return true; + } + + return false; } bool MoveRight () diff --git a/Terminal.Gui/Views/Toplevel.cs b/Terminal.Gui/Views/Toplevel.cs index 2d702ae88f..210618d938 100644 --- a/Terminal.Gui/Views/Toplevel.cs +++ b/Terminal.Gui/Views/Toplevel.cs @@ -23,7 +23,7 @@ namespace Terminal.Gui { /// public partial class Toplevel : View { /// - /// Gets or sets whether the for this is running or not. + /// Gets or sets whether the main loop for this is running or not. /// /// /// Setting this property directly is discouraged. Use instead. @@ -38,7 +38,7 @@ public partial class Toplevel : View { public event EventHandler Loaded; /// - /// Invoked when the has started it's first iteration. + /// Invoked when the main loop has started it's first iteration. /// Subscribe to this event to 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 @@ -138,7 +138,7 @@ internal virtual void OnActivate (Toplevel deactivated) /// /// Called from before the redraws for the first time. /// - virtual public void OnLoaded () + public virtual void OnLoaded () { IsLoaded = true; foreach (Toplevel tl in Subviews.Where (v => v is Toplevel)) { @@ -209,31 +209,42 @@ void SetInitialProperties () AddCommand (Command.NextViewOrTop, () => { MoveNextViewOrTop (); return true; }); AddCommand (Command.PreviousViewOrTop, () => { MovePreviousViewOrTop (); return true; }); AddCommand (Command.Refresh, () => { Application.Refresh (); return true; }); + AddCommand (Command.Accept, () => { + // TODO: Perhaps all views should support the concept of being default? + // TODO: It's bad that Toplevel is tightly coupled with Button + if (Subviews.FirstOrDefault(v => v is Button && ((Button)v).IsDefault && ((Button)v).Enabled) is Button defaultBtn) { + defaultBtn.InvokeCommand (Command.Accept); + return true; + } + return false; + }); // Default keybindings for this view - AddKeyBinding (Application.QuitKey, Command.QuitToplevel); - AddKeyBinding (Key.Z | Key.CtrlMask, Command.Suspend); - - AddKeyBinding (Key.Tab, Command.NextView); - - AddKeyBinding (Key.CursorRight, Command.NextView); - AddKeyBinding (Key.F | Key.CtrlMask, Command.NextView); - - AddKeyBinding (Key.CursorDown, Command.NextView); - AddKeyBinding (Key.I | Key.CtrlMask, Command.NextView); // Unix - - AddKeyBinding (Key.BackTab | Key.ShiftMask, Command.PreviousView); - AddKeyBinding (Key.CursorLeft, Command.PreviousView); - AddKeyBinding (Key.CursorUp, Command.PreviousView); - AddKeyBinding (Key.B | Key.CtrlMask, Command.PreviousView); - - AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextViewOrTop); - AddKeyBinding (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix - - AddKeyBinding (Key.Tab | Key.ShiftMask | Key.CtrlMask, Command.PreviousViewOrTop); - AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix - - AddKeyBinding (Key.L | Key.CtrlMask, Command.Refresh); + KeyBindings.Add ((KeyCode)Application.QuitKey, Command.QuitToplevel); + + KeyBindings.Add (KeyCode.CursorRight, Command.NextView); + KeyBindings.Add (KeyCode.CursorDown, Command.NextView); + KeyBindings.Add (KeyCode.CursorLeft, Command.PreviousView); + KeyBindings.Add (KeyCode.CursorUp, Command.PreviousView); + + KeyBindings.Add (KeyCode.Tab, Command.NextView); + KeyBindings.Add (KeyCode.Tab | KeyCode.ShiftMask, Command.PreviousView); + KeyBindings.Add (KeyCode.Tab | KeyCode.CtrlMask, Command.NextViewOrTop); + KeyBindings.Add (KeyCode.Tab | KeyCode.ShiftMask | KeyCode.CtrlMask, Command.PreviousViewOrTop); + + KeyBindings.Add (KeyCode.F5, Command.Refresh); + KeyBindings.Add ((KeyCode)Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix + KeyBindings.Add ((KeyCode)Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix + +#if UNIX_KEY_BINDINGS + KeyBindings.Add (Key.Z | Key.CtrlMask, Command.Suspend); + KeyBindings.Add (Key.L | Key.CtrlMask, Command.Refresh);// Unix + KeyBindings.Add (Key.F | Key.CtrlMask, Command.NextView);// Unix + KeyBindings.Add (Key.I | Key.CtrlMask, Command.NextView); // Unix + KeyBindings.Add (Key.B | Key.CtrlMask, Command.PreviousView);// Unix +#endif + // This enables the default button to be activated by the Enter key. + KeyBindings.Add (KeyCode.Enter, Command.Accept); } private void Application_UnGrabbingMouse (object sender, GrabMouseEventArgs e) @@ -261,7 +272,7 @@ private void Application_GrabbingMouse (object sender, GrabMouseEventArgs e) /// public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); AlternateForwardKeyChanged?.Invoke (this, e); } @@ -276,7 +287,7 @@ public virtual void OnAlternateForwardKeyChanged (KeyChangedEventArgs e) /// public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); AlternateBackwardKeyChanged?.Invoke (this, e); } @@ -291,7 +302,7 @@ public virtual void OnAlternateBackwardKeyChanged (KeyChangedEventArgs e) /// public virtual void OnQuitKeyChanged (KeyChangedEventArgs e) { - ReplaceKeyBinding (e.OldKey, e.NewKey); + KeyBindings.Replace ((KeyCode)e.OldKey, (KeyCode)e.NewKey); QuitKeyChanged?.Invoke (this, e); } @@ -318,7 +329,7 @@ public override bool CanFocus { /// /// /// - /// events will propagate keys upwards. + /// events will propagate keys upwards. /// /// /// The Toplevel will act as an embedded view (not a modal/pop-up). @@ -329,7 +340,7 @@ public override bool CanFocus { /// /// /// - /// events will NOT propogate keys upwards. + /// events will NOT propagate keys upwards. /// /// /// The Toplevel will and look like a modal (pop-up) (e.g. see . @@ -354,65 +365,6 @@ public override bool CanFocus { /// public bool IsLoaded { get; private set; } - /// - public override bool OnKeyDown (KeyEvent keyEvent) - { - if (base.OnKeyDown (keyEvent)) { - return true; - } - - switch (keyEvent.Key) { - case Key.AltMask: - case Key.AltMask | Key.Space: - case Key.CtrlMask | Key.Space: - case Key _ when (keyEvent.Key & Key.AltMask) == Key.AltMask: - return MenuBar != null && MenuBar.OnKeyDown (keyEvent); - } - - return false; - } - - /// - public override bool OnKeyUp (KeyEvent keyEvent) - { - if (base.OnKeyUp (keyEvent)) { - return true; - } - - switch (keyEvent.Key) { - case Key.AltMask: - case Key.AltMask | Key.Space: - case Key.CtrlMask | Key.Space: - if (MenuBar != null && MenuBar.OnKeyUp (keyEvent)) { - return true; - } - break; - } - - return false; - } - - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (base.ProcessKey (keyEvent)) - return true; - - var result = InvokeKeybindings (new KeyEvent (ShortcutHelper.GetModifiersKey (keyEvent), - new KeyModifiers () { Alt = keyEvent.IsAlt, Ctrl = keyEvent.IsCtrl, Shift = keyEvent.IsShift })); - if (result != null) - return (bool)result; - -#if false - if (keyEvent.Key == Key.F5) { - Application.DebugDrawBounds = !Application.DebugDrawBounds; - SetNeedsDisplay (); - return true; - } -#endif - return false; - } - private void MovePreviousViewOrTop () { if (Application.OverlappedTop == null) { @@ -478,19 +430,6 @@ private void QuitToplevel () } } - /// - public override bool ProcessColdKey (KeyEvent keyEvent) - { - if (base.ProcessColdKey (keyEvent)) { - return true; - } - - if (ShortcutHelper.FindAndOpenByShortcut (keyEvent, this)) { - return true; - } - return false; - } - View GetDeepestFocusedSubview (View view) { if (view == null) { diff --git a/Terminal.Gui/Views/TreeView/Branch.cs b/Terminal.Gui/Views/TreeView/Branch.cs index 83acdf7450..cfcdaa209d 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -195,6 +195,8 @@ public virtual void Draw (ConsoleDriver driver, ColorScheme colorScheme, int y, if (modelScheme != null) { // use it modelColor = isSelected ? modelScheme.Focus : modelScheme.Normal; + } else { + modelColor = new Attribute (); } } diff --git a/Terminal.Gui/Views/TreeView/TreeView.cs b/Terminal.Gui/Views/TreeView/TreeView.cs index 62beaee08d..b5ca8f4829 100644 --- a/Terminal.Gui/Views/TreeView/TreeView.cs +++ b/Terminal.Gui/Views/TreeView/TreeView.cs @@ -14,7 +14,7 @@ namespace Terminal.Gui { /// /// Interface for all non generic members of . /// - /// See TreeView Deep Dive for more information. + /// See TreeView Deep Dive for more information. /// public interface ITreeView { /// @@ -37,7 +37,7 @@ public interface ITreeView { /// Convenience implementation of generic for any tree were all nodes /// implement . /// - /// See TreeView Deep Dive for more information. + /// See TreeView Deep Dive for more information. /// public class TreeView : TreeView { @@ -56,7 +56,7 @@ public TreeView () /// Hierarchical tree view with expandable branches. Branch objects are dynamically determined /// when expanded using a user defined . /// - /// See TreeView Deep Dive for more information. + /// See TreeView Deep Dive for more information. /// public class TreeView : View, ITreeView where T : class { private int scrollOffsetVertical; @@ -119,15 +119,16 @@ public T SelectedObject { /// public event EventHandler> ObjectActivated; + // TODO: Update to use Key instead of KeyCode /// /// Key which when pressed triggers . /// Defaults to Enter. /// - public Key ObjectActivationKey { + public KeyCode ObjectActivationKey { get => objectActivationKey; set { if (objectActivationKey != value) { - ReplaceKeyBinding (ObjectActivationKey, value); + KeyBindings.Replace (ObjectActivationKey, value); objectActivationKey = value; } } @@ -162,7 +163,7 @@ public Key ObjectActivationKey { /// (nodes added but no tree builder set). /// public static string NoBuilderError = "ERROR: TreeBuilder Not Set"; - private Key objectActivationKey = Key.Enter; + private KeyCode objectActivationKey = KeyCode.Enter; /// /// Called when the changes. @@ -286,27 +287,27 @@ public TreeView () : base () AddCommand (Command.Accept, () => { ActivateSelectedObjectIfAny (); return true; }); // Default keybindings for this view - AddKeyBinding (Key.PageUp, Command.PageUp); - AddKeyBinding (Key.PageDown, Command.PageDown); - AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); - AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); - AddKeyBinding (Key.CursorRight, Command.Expand); - AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.ExpandAll); - AddKeyBinding (Key.CursorLeft, Command.Collapse); - AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.CollapseAll); + KeyBindings.Add (KeyCode.PageUp, Command.PageUp); + KeyBindings.Add (KeyCode.PageDown, Command.PageDown); + KeyBindings.Add (KeyCode.PageUp | KeyCode.ShiftMask, Command.PageUpExtend); + KeyBindings.Add (KeyCode.PageDown | KeyCode.ShiftMask, Command.PageDownExtend); + KeyBindings.Add (KeyCode.CursorRight, Command.Expand); + KeyBindings.Add (KeyCode.CursorRight | KeyCode.CtrlMask, Command.ExpandAll); + KeyBindings.Add (KeyCode.CursorLeft, Command.Collapse); + KeyBindings.Add (KeyCode.CursorLeft | KeyCode.CtrlMask, Command.CollapseAll); - AddKeyBinding (Key.CursorUp, Command.LineUp); - AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); - AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.LineUpToFirstBranch); + KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.ShiftMask, Command.LineUpExtend); + KeyBindings.Add (KeyCode.CursorUp | KeyCode.CtrlMask, Command.LineUpToFirstBranch); - AddKeyBinding (Key.CursorDown, Command.LineDown); - AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); - AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.LineDownToLastBranch); + KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.ShiftMask, Command.LineDownExtend); + KeyBindings.Add (KeyCode.CursorDown | KeyCode.CtrlMask, Command.LineDownToLastBranch); - AddKeyBinding (Key.Home, Command.TopHome); - AddKeyBinding (Key.End, Command.BottomEnd); - AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll); - AddKeyBinding (ObjectActivationKey, Command.Accept); + KeyBindings.Add (KeyCode.Home, Command.TopHome); + KeyBindings.Add (KeyCode.End, Command.BottomEnd); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll); + KeyBindings.Add (ObjectActivationKey, Command.Accept); } /// @@ -621,19 +622,14 @@ private IEnumerable> AddToLineMap (Branch currentBranch, bool paren public CollectionNavigator KeystrokeNavigator { get; private set; } = new CollectionNavigator (); /// - public override bool ProcessKey (KeyEvent keyEvent) + public override bool OnProcessKeyDown (Key keyEvent) { if (!Enabled) { return false; } try { - // First of all deal with any registered keybindings - var result = InvokeKeybindings (keyEvent); - if (result != null) { - return (bool)result; - } - + // BUGBUG: this should move to OnInvokingKeyBindings // If not a keybinding, is the key a searchable key press? if (CollectionNavigator.IsCompatibleKey (keyEvent) && AllowLetterBasedNavigation) { IReadOnlyCollection> map; @@ -644,7 +640,7 @@ public override bool ProcessKey (KeyEvent keyEvent) // Find the current selected object within the tree var current = map.IndexOf (b => b.Model == SelectedObject); - var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent.KeyValue); + var newIndex = KeystrokeNavigator?.GetNextMatchingItem (current, (char)keyEvent); if (newIndex is int && newIndex != -1) { SelectedObject = map.ElementAt ((int)newIndex).Model; @@ -654,10 +650,12 @@ public override bool ProcessKey (KeyEvent keyEvent) } } } finally { - PositionCursor (); + if (IsInitialized) { + PositionCursor (); + } } - return base.ProcessKey (keyEvent); + return false; } /// diff --git a/Terminal.Gui/Views/Wizard/Wizard.cs b/Terminal.Gui/Views/Wizard/Wizard.cs index 8b8d0c89bd..722a585c3a 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -4,532 +4,524 @@ using System.Text; using Terminal.Gui.Resources; -namespace Terminal.Gui { - +namespace Terminal.Gui; + +/// +/// Provides navigation and a user interface (UI) to collect related data across multiple steps. Each step () can host +/// arbitrary s, much like a . Each step also has a pane for help text. Along the +/// bottom of the Wizard view are customizable buttons enabling the user to navigate forward and backward through the Wizard. +/// +/// +/// The Wizard can be displayed either as a modal (pop-up) (like ) or as an embedded . +/// +/// By default, is true. In this case launch the Wizard with Application.Run(wizard). +/// +/// See for more details. +/// +/// +/// +/// using Terminal.Gui; +/// using System.Text; +/// +/// Application.Init(); +/// +/// var wizard = new Wizard ($"Setup Wizard"); +/// +/// // Add 1st step +/// var firstStep = new WizardStep ("End User License Agreement"); +/// wizard.AddStep(firstStep); +/// firstStep.NextButtonText = "Accept!"; +/// firstStep.HelpText = "This is the End User License Agreement."; +/// +/// // Add 2nd step +/// var secondStep = new WizardStep ("Second Step"); +/// wizard.AddStep(secondStep); +/// secondStep.HelpText = "This is the help text for the Second Step."; +/// var lbl = new Label ("Name:") { AutoSize = true }; +/// secondStep.Add(lbl); +/// +/// var name = new TextField () { X = Pos.Right (lbl) + 1, Width = Dim.Fill () - 1 }; +/// secondStep.Add(name); +/// +/// wizard.Finished += (args) => +/// { +/// MessageBox.Query("Wizard", $"Finished. The Name entered is '{name.Text}'", "Ok"); +/// Application.RequestStop(); +/// }; +/// +/// Application.Top.Add (wizard); +/// Application.Run (); +/// Application.Shutdown (); +/// +/// +public class Wizard : Dialog { /// - /// Provides navigation and a user interface (UI) to collect related data across multiple steps. Each step () can host - /// arbitrary s, much like a . Each step also has a pane for help text. Along the - /// bottom of the Wizard view are customizable buttons enabling the user to navigate forward and backward through the Wizard. + /// Initializes a new instance of the class using positioning. /// /// - /// The Wizard can be displayed either as a modal (pop-up) (like ) or as an embedded . - /// - /// By default, is true. In this case launch the Wizard with Application.Run(wizard). - /// - /// See for more details. + /// The Wizard will be vertically and horizontally centered in the container. + /// After initialization use X, Y, Width, and Height change size and position. /// - /// - /// - /// using Terminal.Gui; - /// using System.Text; - /// - /// Application.Init(); - /// - /// var wizard = new Wizard ($"Setup Wizard"); - /// - /// // Add 1st step - /// var firstStep = new WizardStep ("End User License Agreement"); - /// wizard.AddStep(firstStep); - /// firstStep.NextButtonText = "Accept!"; - /// firstStep.HelpText = "This is the End User License Agreement."; - /// - /// // Add 2nd step - /// var secondStep = new WizardStep ("Second Step"); - /// wizard.AddStep(secondStep); - /// secondStep.HelpText = "This is the help text for the Second Step."; - /// var lbl = new Label ("Name:") { AutoSize = true }; - /// secondStep.Add(lbl); - /// - /// var name = new TextField () { X = Pos.Right (lbl) + 1, Width = Dim.Fill () - 1 }; - /// secondStep.Add(name); - /// - /// wizard.Finished += (args) => - /// { - /// MessageBox.Query("Wizard", $"Finished. The Name entered is '{name.Text}'", "Ok"); - /// Application.RequestStop(); - /// }; - /// - /// Application.Top.Add (wizard); - /// Application.Run (); - /// Application.Shutdown (); - /// - /// - public class Wizard : Dialog { - - /// - /// Initializes a new instance of the class using positioning. - /// - /// - /// The Wizard will be vertically and horizontally centered in the container. - /// After initialization use X, Y, Width, and Height change size and position. - /// - public Wizard () : base () - { - - // Using Justify causes the Back and Next buttons to be hard justified against - // the left and right edge - ButtonAlignment = ButtonAlignments.Justify; - BorderStyle = LineStyle.Double; - - //// Add a horiz separator - var separator = new LineView (Orientation.Horizontal) { - Y = Pos.AnchorEnd (2) - }; - Add (separator); - - // BUGBUG: Space is to work around https://github.com/gui-cs/Terminal.Gui/issues/1812 - backBtn = new Button (Strings.wzBack) { AutoSize = true }; - AddButton (backBtn); - - nextfinishBtn = new Button (Strings.wzFinish) { AutoSize = true }; - nextfinishBtn.IsDefault = true; - AddButton (nextfinishBtn); - - backBtn.Clicked += BackBtn_Clicked; - nextfinishBtn.Clicked += NextfinishBtn_Clicked; - - Loaded += Wizard_Loaded; - Closing += Wizard_Closing; - TitleChanged += Wizard_TitleChanged; - - if (Modal) { - ClearKeyBinding (Command.QuitToplevel); - AddKeyBinding (Key.Esc, Command.QuitToplevel); - } - SetNeedsLayout (); - + public Wizard () : base () + { + + // Using Justify causes the Back and Next buttons to be hard justified against + // the left and right edge + ButtonAlignment = ButtonAlignments.Justify; + BorderStyle = LineStyle.Double; + + //// Add a horiz separator + var separator = new LineView (Orientation.Horizontal) { + Y = Pos.AnchorEnd (2) + }; + Add (separator); + + // BUGBUG: Space is to work around https://github.com/gui-cs/Terminal.Gui/issues/1812 + backBtn = new Button (Strings.wzBack) { AutoSize = true }; + AddButton (backBtn); + + nextfinishBtn = new Button (Strings.wzFinish) { AutoSize = true }; + nextfinishBtn.IsDefault = true; + AddButton (nextfinishBtn); + + backBtn.Clicked += BackBtn_Clicked; + nextfinishBtn.Clicked += NextfinishBtn_Clicked; + + Loaded += Wizard_Loaded; + Closing += Wizard_Closing; + TitleChanged += Wizard_TitleChanged; + + if (Modal) { + KeyBindings.Clear (Command.QuitToplevel); + KeyBindings.Add (KeyCode.Esc, Command.QuitToplevel); } + SetNeedsLayout (); - private void Wizard_TitleChanged (object sender, TitleEventArgs e) - { - if (string.IsNullOrEmpty (wizardTitle)) { - wizardTitle = e.NewTitle; - } - } + } - private void Wizard_Loaded (object sender, EventArgs args) - { - CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null + void Wizard_TitleChanged (object sender, TitleEventArgs e) + { + if (string.IsNullOrEmpty (wizardTitle)) { + wizardTitle = e.NewTitle; } + } - private bool finishedPressed = false; + void Wizard_Loaded (object sender, EventArgs args) => CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null - private void Wizard_Closing (object sender, ToplevelClosingEventArgs obj) - { - if (!finishedPressed) { - var args = new WizardButtonEventArgs (); - Cancelled?.Invoke (this, args); - } - } + bool finishedPressed = false; - private void NextfinishBtn_Clicked (object sender, EventArgs e) - { - if (CurrentStep == GetLastStep ()) { - var args = new WizardButtonEventArgs (); - Finished?.Invoke (this, args); - if (!args.Cancel) { - finishedPressed = true; - if (IsCurrentTop) { - Application.RequestStop (this); - } else { - // Wizard was created as a non-modal (just added to another View). - // Do nothing - } - } - } else { - var args = new WizardButtonEventArgs (); - MovingNext?.Invoke (this, args); - if (!args.Cancel) { - GoNext (); - } - } + void Wizard_Closing (object sender, ToplevelClosingEventArgs obj) + { + if (!finishedPressed) { + var args = new WizardButtonEventArgs (); + Cancelled?.Invoke (this, args); } + } - /// - /// is derived from and Dialog causes Esc to call - /// , closing the Dialog. Wizard overrides - /// to instead fire the event when Wizard is being used as a non-modal (see . - /// See for more. - /// - /// - /// - public override bool ProcessKey (KeyEvent kb) - { - if (!Modal) { - switch (kb.Key) { - case Key.Esc: - var args = new WizardButtonEventArgs (); - Cancelled?.Invoke (this, args); - return false; + void NextfinishBtn_Clicked (object sender, EventArgs e) + { + if (CurrentStep == GetLastStep ()) { + var args = new WizardButtonEventArgs (); + Finished?.Invoke (this, args); + if (!args.Cancel) { + finishedPressed = true; + if (IsCurrentTop) { + Application.RequestStop (this); + } else { + // Wizard was created as a non-modal (just added to another View). + // Do nothing } } - return base.ProcessKey (kb); + } else { + var args = new WizardButtonEventArgs (); + MovingNext?.Invoke (this, args); + if (!args.Cancel) { + GoNext (); + } } + } - /// - /// Causes the wizad to move to the next enabled step (or last step if is not set). - /// If there is no previous step, does nothing. - /// - public void GoNext () - { - var nextStep = GetNextStep (); - if (nextStep != null) { - GoToStep (nextStep); + /// + /// is derived from and Dialog causes Esc to call + /// , closing the Dialog. Wizard overrides + /// to instead fire the event when Wizard is being used as a non-modal (see . + /// + /// + /// + public override bool OnProcessKeyDown (Key a) + { + //// BUGBUG: Why is this not handled by a key binding??? + if (!Modal) { + switch (a.KeyCode) { + // BUGBUG: This should be handled by Dialog + case KeyCode.Esc: + var args = new WizardButtonEventArgs (); + Cancelled?.Invoke (this, args); + return false; } } + return false; + } - /// - /// Returns the next enabled after the current step. Takes into account steps which - /// are disabled. If is null returns the first enabled step. - /// - /// The next step after the current step, if there is one; otherwise returns null, which - /// indicates either there are no enabled steps or the current step is the last enabled step. - public WizardStep GetNextStep () - { - LinkedListNode step = null; - if (CurrentStep == null) { - // Get first step, assume it is next - step = steps.First; - } else { - // Get the step after current - step = steps.Find (CurrentStep); - if (step != null) { - step = step.Next; - } - } + /// + /// Causes the wizad to move to the next enabled step (or last step if is not set). + /// If there is no previous step, does nothing. + /// + public void GoNext () + { + var nextStep = GetNextStep (); + if (nextStep != null) { + GoToStep (nextStep); + } + } - // step now points to the potential next step - while (step != null) { - if (step.Value.Enabled) { - return step.Value; - } + /// + /// Returns the next enabled after the current step. Takes into account steps which + /// are disabled. If is null returns the first enabled step. + /// + /// The next step after the current step, if there is one; otherwise returns null, which + /// indicates either there are no enabled steps or the current step is the last enabled step. + public WizardStep GetNextStep () + { + LinkedListNode step = null; + if (CurrentStep == null) { + // Get first step, assume it is next + step = steps.First; + } else { + // Get the step after current + step = steps.Find (CurrentStep); + if (step != null) { step = step.Next; } - return null; } - private void BackBtn_Clicked (object sender, EventArgs e) - { - var args = new WizardButtonEventArgs (); - MovingBack?.Invoke (this, args); - if (!args.Cancel) { - GoBack (); + // step now points to the potential next step + while (step != null) { + if (step.Value.Enabled) { + return step.Value; } + step = step.Next; } + return null; + } - /// - /// Causes the wizad to move to the previous enabled step (or first step if is not set). - /// If there is no previous step, does nothing. - /// - public void GoBack () - { - var previous = GetPreviousStep (); - if (previous != null) { - GoToStep (previous); - } + void BackBtn_Clicked (object sender, EventArgs e) + { + var args = new WizardButtonEventArgs (); + MovingBack?.Invoke (this, args); + if (!args.Cancel) { + GoBack (); } + } - /// - /// Returns the first enabled before the current step. Takes into account steps which - /// are disabled. If is null returns the last enabled step. - /// - /// The first step ahead of the current step, if there is one; otherwise returns null, which - /// indicates either there are no enabled steps or the current step is the first enabled step. - public WizardStep GetPreviousStep () - { - LinkedListNode step = null; - if (CurrentStep == null) { - // Get last step, assume it is previous - step = steps.Last; - } else { - // Get the step before current - step = steps.Find (CurrentStep); - if (step != null) { - step = step.Previous; - } - } + /// + /// Causes the wizad to move to the previous enabled step (or first step if is not set). + /// If there is no previous step, does nothing. + /// + public void GoBack () + { + var previous = GetPreviousStep (); + if (previous != null) { + GoToStep (previous); + } + } - // step now points to the potential previous step - while (step != null) { - if (step.Value.Enabled) { - return step.Value; - } + /// + /// Returns the first enabled before the current step. Takes into account steps which + /// are disabled. If is null returns the last enabled step. + /// + /// The first step ahead of the current step, if there is one; otherwise returns null, which + /// indicates either there are no enabled steps or the current step is the first enabled step. + public WizardStep GetPreviousStep () + { + LinkedListNode step = null; + if (CurrentStep == null) { + // Get last step, assume it is previous + step = steps.Last; + } else { + // Get the step before current + step = steps.Find (CurrentStep); + if (step != null) { step = step.Previous; } - return null; } - /// - /// Returns the first enabled step in the Wizard - /// - /// The last enabled step - public WizardStep GetFirstStep () - { - return steps.FirstOrDefault (s => s.Enabled); + // step now points to the potential previous step + while (step != null) { + if (step.Value.Enabled) { + return step.Value; + } + step = step.Previous; } + return null; + } - /// - /// Returns the last enabled step in the Wizard - /// - /// The last enabled step - public WizardStep GetLastStep () - { - return steps.LastOrDefault (s => s.Enabled); - } + /// + /// Returns the first enabled step in the Wizard + /// + /// The last enabled step + public WizardStep GetFirstStep () => steps.FirstOrDefault (s => s.Enabled); - private LinkedList steps = new LinkedList (); - private WizardStep currentStep = null; - - /// - /// If the is not the first step in the wizard, this button causes - /// the event to be fired and the wizard moves to the previous step. - /// - /// - /// Use the event to be notified when the user attempts to go back. - /// - public Button BackButton { get => backBtn; } - private Button backBtn; - - /// - /// If the is the last step in the wizard, this button causes - /// the event to be fired and the wizard to close. If the step is not the last step, - /// the event will be fired and the wizard will move next step. - /// - /// - /// Use the and events to be notified - /// when the user attempts go to the next step or finish the wizard. - /// - public Button NextFinishButton { get => nextfinishBtn; } - private Button nextfinishBtn; - - /// - /// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the - /// order they were added. - /// - /// - /// The "Next..." button of the last step added will read "Finish" (unless changed from default). - public void AddStep (WizardStep newStep) - { - SizeStep (newStep); - - newStep.EnabledChanged += (s, e) => UpdateButtonsAndTitle (); - newStep.TitleChanged += (s, e) => UpdateButtonsAndTitle (); - steps.AddLast (newStep); - this.Add (newStep); - UpdateButtonsAndTitle (); - } + /// + /// Returns the last enabled step in the Wizard + /// + /// The last enabled step + public WizardStep GetLastStep () => steps.LastOrDefault (s => s.Enabled); - ///// - ///// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended. - ///// - ///// - ///// The Title is only displayed when the is set to false. - ///// - //public new string Title { - // get { - // // The base (Dialog) Title holds the full title ("Wizard Title - Step Title") - // return base.Title; - // } - // set { - // wizardTitle = value; - // base.Title = $"{wizardTitle}{(steps.Count > 0 && currentStep != null ? " - " + currentStep.Title : string.Empty)}"; - // } - //} - private string wizardTitle = string.Empty; - - /// - /// Raised when the Back button in the is clicked. The Back button is always - /// the first button in the array of Buttons passed to the constructor, if any. - /// - public event EventHandler MovingBack; - - /// - /// Raised when the Next/Finish button in the is clicked (or the user presses Enter). - /// The Next/Finish button is always the last button in the array of Buttons passed to the constructor, - /// if any. This event is only raised if the is the last Step in the Wizard flow - /// (otherwise the event is raised). - /// - public event EventHandler MovingNext; - - /// - /// Raised when the Next/Finish button in the is clicked. The Next/Finish button is always - /// the last button in the array of Buttons passed to the constructor, if any. This event is only - /// raised if the is the last Step in the Wizard flow - /// (otherwise the event is raised). - /// - public event EventHandler Finished; - - /// - /// Raised when the user has cancelled the by pressin the Esc key. - /// To prevent a modal ( is true) Wizard from - /// closing, cancel the event by setting to - /// true before returning from the event handler. - /// - public event EventHandler Cancelled; - - /// - /// This event is raised when the current ) is about to change. Use - /// to abort the transition. - /// - public event EventHandler StepChanging; - - /// - /// This event is raised after the has changed the . - /// - public event EventHandler StepChanged; - - /// - /// Gets or sets the currently active . - /// - public WizardStep CurrentStep { - get => currentStep; - set { - GoToStep (value); - } - } + LinkedList steps = new (); + WizardStep currentStep = null; - /// - /// Called when the is about to transition to another . Fires the event. - /// - /// The step the Wizard is about to change from - /// The step the Wizard is about to change to - /// True if the change is to be cancelled. - public virtual bool OnStepChanging (WizardStep oldStep, WizardStep newStep) - { - var args = new StepChangeEventArgs (oldStep, newStep); - StepChanging?.Invoke (this, args); - return args.Cancel; - } + /// + /// If the is not the first step in the wizard, this button causes + /// the event to be fired and the wizard moves to the previous step. + /// + /// + /// Use the event to be notified when the user attempts to go back. + /// + public Button BackButton => backBtn; - /// - /// Called when the has completed transition to a new . Fires the event. - /// - /// The step the Wizard changed from - /// The step the Wizard has changed to - /// True if the change is to be cancelled. - public virtual bool OnStepChanged (WizardStep oldStep, WizardStep newStep) - { - var args = new StepChangeEventArgs (oldStep, newStep); - StepChanged?.Invoke (this, args); - return args.Cancel; - } + Button backBtn; - /// - /// Changes to the specified . - /// - /// The step to go to. - /// True if the transition to the step succeeded. False if the step was not found or the operation was cancelled. - public bool GoToStep (WizardStep newStep) - { - if (OnStepChanging (currentStep, newStep) || (newStep != null && !newStep.Enabled)) { - return false; - } + /// + /// If the is the last step in the wizard, this button causes + /// the event to be fired and the wizard to close. If the step is not the last step, + /// the event will be fired and the wizard will move next step. + /// + /// + /// Use the and events to be notified + /// when the user attempts go to the next step or finish the wizard. + /// + public Button NextFinishButton => nextfinishBtn; - // Hide all but the new step - foreach (WizardStep step in steps) { - step.Visible = (step == newStep); - step.ShowHide (); - } + Button nextfinishBtn; - var oldStep = currentStep; - currentStep = newStep; + /// + /// Adds a step to the wizard. The Next and Back buttons navigate through the added steps in the + /// order they were added. + /// + /// + /// The "Next..." button of the last step added will read "Finish" (unless changed from default). + public void AddStep (WizardStep newStep) + { + SizeStep (newStep); + + newStep.EnabledChanged += (s, e) => UpdateButtonsAndTitle (); + newStep.TitleChanged += (s, e) => UpdateButtonsAndTitle (); + steps.AddLast (newStep); + Add (newStep); + UpdateButtonsAndTitle (); + } - UpdateButtonsAndTitle (); + ///// + ///// The title of the Wizard, shown at the top of the Wizard with " - currentStep.Title" appended. + ///// + ///// + ///// The Title is only displayed when the is set to false. + ///// + //public new string Title { + // get { + // // The base (Dialog) Title holds the full title ("Wizard Title - Step Title") + // return base.Title; + // } + // set { + // wizardTitle = value; + // base.Title = $"{wizardTitle}{(steps.Count > 0 && currentStep != null ? " - " + currentStep.Title : string.Empty)}"; + // } + //} + string wizardTitle = string.Empty; - // Set focus to the nav buttons - if (backBtn.HasFocus) { - backBtn.SetFocus (); - } else { - nextfinishBtn.SetFocus (); - } + /// + /// Raised when the Back button in the is clicked. The Back button is always + /// the first button in the array of Buttons passed to the constructor, if any. + /// + public event EventHandler MovingBack; - if (OnStepChanged (oldStep, currentStep)) { - // For correctness we do this, but it's meaningless because there's nothing to cancel - return false; - } + /// + /// Raised when the Next/Finish button in the is clicked (or the user presses Enter). + /// The Next/Finish button is always the last button in the array of Buttons passed to the constructor, + /// if any. This event is only raised if the is the last Step in the Wizard flow + /// (otherwise the event is raised). + /// + public event EventHandler MovingNext; + + /// + /// Raised when the Next/Finish button in the is clicked. The Next/Finish button is always + /// the last button in the array of Buttons passed to the constructor, if any. This event is only + /// raised if the is the last Step in the Wizard flow + /// (otherwise the event is raised). + /// + public event EventHandler Finished; + + /// + /// Raised when the user has cancelled the by pressin the Esc key. + /// To prevent a modal ( is true) Wizard from + /// closing, cancel the event by setting to + /// true before returning from the event handler. + /// + public event EventHandler Cancelled; + + /// + /// This event is raised when the current ) is about to change. Use + /// to abort the transition. + /// + public event EventHandler StepChanging; + + /// + /// This event is raised after the has changed the . + /// + public event EventHandler StepChanged; + + /// + /// Gets or sets the currently active . + /// + public WizardStep CurrentStep { + get => currentStep; + set => GoToStep (value); + } + + /// + /// Called when the is about to transition to another . Fires the event. + /// + /// The step the Wizard is about to change from + /// The step the Wizard is about to change to + /// True if the change is to be cancelled. + public virtual bool OnStepChanging (WizardStep oldStep, WizardStep newStep) + { + var args = new StepChangeEventArgs (oldStep, newStep); + StepChanging?.Invoke (this, args); + return args.Cancel; + } + + /// + /// Called when the has completed transition to a new . Fires the event. + /// + /// The step the Wizard changed from + /// The step the Wizard has changed to + /// True if the change is to be cancelled. + public virtual bool OnStepChanged (WizardStep oldStep, WizardStep newStep) + { + var args = new StepChangeEventArgs (oldStep, newStep); + StepChanged?.Invoke (this, args); + return args.Cancel; + } + + /// + /// Changes to the specified . + /// + /// The step to go to. + /// True if the transition to the step succeeded. False if the step was not found or the operation was cancelled. + public bool GoToStep (WizardStep newStep) + { + if (OnStepChanging (currentStep, newStep) || newStep != null && !newStep.Enabled) { + return false; + } - return true; + // Hide all but the new step + foreach (var step in steps) { + step.Visible = step == newStep; + step.ShowHide (); } - private void UpdateButtonsAndTitle () - { - if (CurrentStep == null) return; + var oldStep = currentStep; + currentStep = newStep; - Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + CurrentStep.Title : string.Empty)}"; + UpdateButtonsAndTitle (); - // Configure the Back button - backBtn.Text = CurrentStep.BackButtonText != string.Empty ? CurrentStep.BackButtonText : Strings.wzBack; // "_Back"; - backBtn.Visible = (CurrentStep != GetFirstStep ()); + // Set focus to the nav buttons + if (backBtn.HasFocus) { + backBtn.SetFocus (); + } else { + nextfinishBtn.SetFocus (); + } - // Configure the Next/Finished button - if (CurrentStep == GetLastStep ()) { - nextfinishBtn.Text = CurrentStep.NextButtonText != string.Empty ? CurrentStep.NextButtonText : Strings.wzFinish; // "Fi_nish"; - } else { - nextfinishBtn.Text = CurrentStep.NextButtonText != string.Empty ? CurrentStep.NextButtonText : Strings.wzNext; // "_Next..."; - } + if (OnStepChanged (oldStep, currentStep)) { + // For correctness we do this, but it's meaningless because there's nothing to cancel + return false; + } - SizeStep (CurrentStep); + return true; + } - SetNeedsLayout (); - LayoutSubviews (); - Draw (); + void UpdateButtonsAndTitle () + { + if (CurrentStep == null) { + return; } - private void SizeStep (WizardStep step) - { - if (Modal) { - // If we're modal, then we expand the WizardStep so that the top and side - // borders and not visible. The bottom border is the separator above the buttons. - step.X = step.Y = 0; - step.Height = Dim.Fill (2); // for button frame - step.Width = Dim.Fill (0); - } else { - // If we're not a modal, then we show the border around the WizardStep - step.X = step.Y = 0; - step.Height = Dim.Fill (1); // for button frame - step.Width = Dim.Fill (0); - } + Title = $"{wizardTitle}{(steps.Count > 0 ? " - " + CurrentStep.Title : string.Empty)}"; + + // Configure the Back button + backBtn.Text = CurrentStep.BackButtonText != string.Empty ? CurrentStep.BackButtonText : Strings.wzBack; // "_Back"; + backBtn.Visible = CurrentStep != GetFirstStep (); + + // Configure the Next/Finished button + if (CurrentStep == GetLastStep ()) { + nextfinishBtn.Text = CurrentStep.NextButtonText != string.Empty ? CurrentStep.NextButtonText : Strings.wzFinish; // "Fi_nish"; + } else { + nextfinishBtn.Text = CurrentStep.NextButtonText != string.Empty ? CurrentStep.NextButtonText : Strings.wzNext; // "_Next..."; } - /// - /// Determines whether the is displayed as modal pop-up or not. - /// - /// The default is . The Wizard will be shown with a frame and title and will behave like - /// any window. - /// - /// If set to false the Wizard will have no frame and will behave like any embedded . - /// - /// To use Wizard as an embedded View - /// - /// Set to false. - /// Add the Wizard to a containing view with . - /// - /// - /// If a non-Modal Wizard is added to the application after has been called - /// the first step must be explicitly set by setting to : - /// - /// wizard.CurrentStep = wizard.GetNextStep(); - /// - /// - public new bool Modal { - get => base.Modal; - set { - base.Modal = value; - foreach (var step in steps) { - SizeStep (step); - } - if (base.Modal) { - ColorScheme = Colors.Dialog; - BorderStyle = LineStyle.Rounded; + SizeStep (CurrentStep); + + SetNeedsLayout (); + LayoutSubviews (); + Draw (); + } + + void SizeStep (WizardStep step) + { + if (Modal) { + // If we're modal, then we expand the WizardStep so that the top and side + // borders and not visible. The bottom border is the separator above the buttons. + step.X = step.Y = 0; + step.Height = Dim.Fill (2); // for button frame + step.Width = Dim.Fill (0); + } else { + // If we're not a modal, then we show the border around the WizardStep + step.X = step.Y = 0; + step.Height = Dim.Fill (1); // for button frame + step.Width = Dim.Fill (0); + } + } + + /// + /// Determines whether the is displayed as modal pop-up or not. + /// + /// The default is . The Wizard will be shown with a frame and title and will behave like + /// any window. + /// + /// If set to false the Wizard will have no frame and will behave like any embedded . + /// + /// To use Wizard as an embedded View + /// + /// Set to false. + /// Add the Wizard to a containing view with . + /// + /// + /// If a non-Modal Wizard is added to the application after has been called + /// the first step must be explicitly set by setting to : + /// + /// wizard.CurrentStep = wizard.GetNextStep(); + /// + /// + public new bool Modal { + get => base.Modal; + set { + base.Modal = value; + foreach (var step in steps) { + SizeStep (step); + } + if (base.Modal) { + ColorScheme = Colors.Dialog; + BorderStyle = LineStyle.Rounded; + } else { + if (SuperView != null) { + ColorScheme = SuperView.ColorScheme; } else { - if (SuperView != null) { - ColorScheme = SuperView.ColorScheme; - } else { - ColorScheme = Colors.Base; - } - CanFocus = true; - BorderStyle = LineStyle.None; + ColorScheme = Colors.Base; } + CanFocus = true; + BorderStyle = LineStyle.None; } } } diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs index 538320f383..306e295392 100644 --- a/UICatalog/KeyBindingsDialog.cs +++ b/UICatalog/KeyBindingsDialog.cs @@ -8,8 +8,8 @@ namespace UICatalog { class KeyBindingsDialog : Dialog { - - static Dictionary CurrentBindings = new Dictionary(); + // TODO: Update to use Key instead of KeyCode + static Dictionary CurrentBindings = new Dictionary(); private Command[] commands; private ListView commandsListView; private Label keyLabel; @@ -28,7 +28,7 @@ private class ViewTracker { Dictionary knownViews = new Dictionary (); private object lockKnownViews = new object (); - private Dictionary keybindings; + private Dictionary keybindings; public ViewTracker (View top) { @@ -70,7 +70,7 @@ internal static void Initialize () Instance = new ViewTracker (Application.Top); } - internal void StartUsingNewKeyMap (Dictionary currentBindings) + internal void StartUsingNewKeyMap (Dictionary currentBindings) { lock (lockKnownViews) { @@ -109,8 +109,8 @@ private void ApplyKeyBindingsToAllKnownViews () if(supported.Contains(kvp.Key)) { // if the key was bound to any other commands clear that - view.ClearKeyBinding (kvp.Key); - view.AddKeyBinding (kvp.Value,kvp.Key); + view.KeyBindings.Remove (kvp.Value); + view.KeyBindings.Add (kvp.Value,kvp.Key); } // mark that we have done this view so don't need to set keybindings again on it @@ -176,12 +176,12 @@ public KeyBindingsDialog () : base() private void RemapKey (object sender, EventArgs e) { var cmd = commands [commandsListView.SelectedItem]; - Key? key = null; + KeyCode? key = null; // prompt user to hit a key var dlg = new Dialog () { Title = "Enter Key" }; - dlg.KeyPressed += (s, k) => { - key = k.KeyEvent.Key; + dlg.KeyDown += (s, k) => { + key = k.KeyCode; Application.RequestStop (); }; Application.Run (dlg); diff --git a/UICatalog/Properties/launchSettings.json b/UICatalog/Properties/launchSettings.json index b06e2ce0bc..4b3e5f0273 100644 --- a/UICatalog/Properties/launchSettings.json +++ b/UICatalog/Properties/launchSettings.json @@ -60,6 +60,10 @@ }, "Docker": { "commandName": "Docker" + }, + "MenuBarScenario": { + "commandName": "Project", + "commandLineArgs": "MenuBar" } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/ASCIICustomButton.cs b/UICatalog/Scenarios/ASCIICustomButton.cs index eabde34c57..cb2250a1f2 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -21,7 +21,7 @@ public override void Init () CheckType = MenuItemCheckStyle.Checked }, null, - new MenuItem("Quit", "",() => Application.RequestStop(),null,null, Application.QuitKey) + new MenuItem("Quit", "",() => Application.RequestStop(),null,null, (KeyCode)Application.QuitKey) }) }); Application.Top.Add (menu, _scrollViewTestWindow); @@ -193,7 +193,7 @@ public ScrollViewTestWindow () }; } - scrollView.ClearKeyBindings (); + scrollView.KeyBindings.Clear (); buttons = new List -[ScenarioMetadata (Name: "Character Map", Description: "Unicode viewer demonstrating the ScrollView control.")] +[ScenarioMetadata ("Character Map", "Unicode viewer demonstrating the ScrollView control.")] [ScenarioCategory ("Text and Formatting")] [ScenarioCategory ("Controls")] [ScenarioCategory ("ScrollView")] @@ -48,9 +48,15 @@ public override void Setup () }; Application.Top.Add (_charMap); - var jumpLabel = new Label ("Jump To Code Point:") { X = Pos.Right (_charMap) + 1, Y = Pos.Y (_charMap) }; + var jumpLabel = new Label ("_Jump To Code Point:") { + X = Pos.Right (_charMap) + 1, + Y = Pos.Y (_charMap), + HotKeySpecifier = (Rune)'_' + }; Application.Top.Add (jumpLabel); - var jumpEdit = new TextField () { X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3" }; + var jumpEdit = new TextField () { + X = Pos.Right (jumpLabel) + 1, Y = Pos.Y (_charMap), Width = 10, Caption = "e.g. 01BE3" + }; Application.Top.Add (jumpEdit); _errorLabel = new Label ("err") { X = Pos.Right (jumpEdit) + 1, Y = Pos.Y (_charMap), ColorScheme = Colors.ColorSchemes ["error"] }; Application.Top.Add (_errorLabel); @@ -72,7 +78,7 @@ public override void Setup () //jumpList.Style.ShowVerticalHeaderLines = false; _categoryList.Style.AlwaysShowHeaders = true; - var isDescending = false; + bool isDescending = false; _categoryList.Table = CreateCategoryTable (0, isDescending); @@ -81,19 +87,19 @@ public override void Setup () _categoryList.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol); if (clickedCol != null && e.MouseEvent.Flags.HasFlag (MouseFlags.Button1Clicked)) { var table = (EnumerableTableSource)_categoryList.Table; - var prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category; + string prevSelection = table.Data.ElementAt (_categoryList.SelectedRow).Category; isDescending = !isDescending; _categoryList.Table = CreateCategoryTable (clickedCol.Value, isDescending); table = (EnumerableTableSource)_categoryList.Table; _categoryList.SelectedRow = table.Data - .Select ((item, index) => new { item, index }) - .FirstOrDefault (x => x.item.Category == prevSelection)?.index ?? -1; + .Select ((item, index) => new { item, index }) + .FirstOrDefault (x => x.item.Category == prevSelection)?.index ?? -1; } }; - var longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ()); + int longestName = UnicodeRange.Ranges.Max (r => r.Category.GetColumns ()); _categoryList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }); _categoryList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1, MinWidth = 6 }); _categoryList.Style.ColumnStyles.Add (2, new ColumnStyle () { MaxWidth = 1, MinWidth = 6 }); @@ -101,7 +107,7 @@ public override void Setup () _categoryList.Width = _categoryList.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 4; _categoryList.SelectedCellChanged += (s, args) => { - EnumerableTableSource table = (EnumerableTableSource)_categoryList.Table; + var table = (EnumerableTableSource)_categoryList.Table; _charMap.StartCodePoint = table.Data.ToArray () [args.NewRow].Start; }; @@ -114,11 +120,11 @@ public override void Setup () _charMap.Width = Dim.Fill () - _categoryList.Width; var menu = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", $"{Application.QuitKey}", () => Application.RequestStop ()), + new ("_File", new MenuItem [] { + new ("_Quit", $"{Application.QuitKey}", () => Application.RequestStop ()) }), - new MenuBarItem ("_Options", new MenuItem [] { - CreateMenuShowWidth (), + new ("_Options", new MenuItem [] { + CreateMenuShowWidth () }) }); Application.Top.Add (menu); @@ -131,7 +137,7 @@ public override void Setup () MenuItem CreateMenuShowWidth () { var item = new MenuItem { - Title = "_Show Glyph Width", + Title = "_Show Glyph Width" }; item.CheckType |= MenuItemCheckStyle.Checked; item.Checked = _charMap?.ShowGlyphWidths; @@ -145,11 +151,11 @@ MenuItem CreateMenuShowWidth () EnumerableTableSource CreateCategoryTable (int sortByColumn, bool descending) { Func orderBy; - var categorySort = string.Empty; - var startSort = string.Empty; - var endSort = string.Empty; + string categorySort = string.Empty; + string startSort = string.Empty; + string endSort = string.Empty; - var sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString (); + string sortIndicator = descending ? CM.Glyphs.DownArrow.ToString () : CM.Glyphs.UpArrow.ToString (); switch (sortByColumn) { case 0: orderBy = r => r.Category; @@ -167,19 +173,18 @@ EnumerableTableSource CreateCategoryTable (int sortByColumn, bool throw new ArgumentException ("Invalid column number."); } - IOrderedEnumerable sortedRanges = descending ? + var sortedRanges = descending ? UnicodeRange.Ranges.OrderByDescending (orderBy) : UnicodeRange.Ranges.OrderBy (orderBy); - return new EnumerableTableSource (sortedRanges, new Dictionary> () - { + return new EnumerableTableSource (sortedRanges, new Dictionary> () { { $"Category{categorySort}", s => s.Category }, { $"Start{startSort}", s => $"{s.Start:x5}" }, - { $"End{endSort}", s => $"{s.End:x5}" }, + { $"End{endSort}", s => $"{s.End:x5}" } }); } - private void JumpEdit_TextChanged (object sender, TextChangedEventArgs e) + void JumpEdit_TextChanged (object sender, TextChangedEventArgs e) { var jumpEdit = sender as TextField; if (jumpEdit.Text.Length == 0) { @@ -217,8 +222,8 @@ private void JumpEdit_TextChanged (object sender, TextChangedEventArgs e) var table = (EnumerableTableSource)_categoryList.Table; _categoryList.SelectedRow = table.Data - .Select ((item, index) => new { item, index }) - .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)?.index ?? -1; + .Select ((item, index) => new { item, index }) + .FirstOrDefault (x => x.item.Start <= result && x.item.End >= result)?.index ?? -1; _categoryList.EnsureSelectedCellIsVisible (); // Ensure the typed glyph is selected @@ -227,7 +232,6 @@ private void JumpEdit_TextChanged (object sender, TextChangedEventArgs e) } class CharMap : ScrollView { - /// /// Specifies the starting offset for the character map. The default is 0x2500 /// which is the Box Drawing characters. @@ -251,10 +255,10 @@ public int SelectedCodePoint { get => _selected; set { _selected = value; - var row = (SelectedCodePoint / 16 * _rowHeight); - var col = (SelectedCodePoint % 16 * COLUMN_WIDTH); + int row = SelectedCodePoint / 16 * _rowHeight; + int col = SelectedCodePoint % 16 * COLUMN_WIDTH; - var height = (Bounds.Height) - (ShowHorizontalScrollIndicator ? 2 : 1); + int height = Bounds.Height - (ShowHorizontalScrollIndicator ? 2 : 1); if (row + ContentOffset.Y < 0) { // Moving up. ContentOffset = new Point (ContentOffset.X, row); @@ -262,7 +266,7 @@ public int SelectedCodePoint { // Moving down. ContentOffset = new Point (ContentOffset.X, Math.Min (row, row - height + _rowHeight)); } - var width = (Bounds.Width / COLUMN_WIDTH * COLUMN_WIDTH) - (ShowVerticalScrollIndicator ? RowLabelWidth + 1 : RowLabelWidth); + int width = Bounds.Width / COLUMN_WIDTH * COLUMN_WIDTH - (ShowVerticalScrollIndicator ? RowLabelWidth + 1 : RowLabelWidth); if (col + ContentOffset.X < 0) { // Moving left. ContentOffset = new Point (col, ContentOffset.Y); @@ -282,8 +286,8 @@ public int SelectedCodePoint { /// public Point Cursor { get { - var row = (SelectedCodePoint / 16 * _rowHeight) + ContentOffset.Y + 1; - var col = (SelectedCodePoint % 16 * COLUMN_WIDTH) + ContentOffset.X + RowLabelWidth + 1; // + 1 for padding + int row = SelectedCodePoint / 16 * _rowHeight + ContentOffset.Y + 1; + int col = SelectedCodePoint % 16 * COLUMN_WIDTH + ContentOffset.X + RowLabelWidth + 1; // + 1 for padding return new Point (col, row); } set => throw new NotImplementedException (); @@ -292,10 +296,10 @@ public Point Cursor { public override void PositionCursor () { if (HasFocus && - Cursor.X >= RowLabelWidth && - Cursor.X < Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0) && - Cursor.Y > 0 && - Cursor.Y < Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0)) { + Cursor.X >= RowLabelWidth && + Cursor.X < Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0) && + Cursor.Y > 0 && + Cursor.Y < Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0)) { Driver.SetCursorVisibility (CursorVisibility.Default); Move (Cursor.X, Cursor.Y); } else { @@ -320,13 +324,14 @@ public bool ShowGlyphWidths { public static int MaxCodePoint => 0x10FFFF; static int RowLabelWidth => $"U+{MaxCodePoint:x5}".Length + 1; - static int RowWidth => RowLabelWidth + (COLUMN_WIDTH * 16); + + static int RowWidth => RowLabelWidth + COLUMN_WIDTH * 16; public CharMap () { ColorScheme = Colors.Dialog; CanFocus = true; - ContentSize = new Size (CharMap.RowWidth, (int)((MaxCodePoint / 16 + (ShowHorizontalScrollIndicator ? 2 : 1)) * _rowHeight)); + ContentSize = new Size (RowWidth, (int)((MaxCodePoint / 16 + (ShowHorizontalScrollIndicator ? 2 : 1)) * _rowHeight)); AddCommand (Command.ScrollUp, () => { if (SelectedCodePoint >= 16) { @@ -353,12 +358,12 @@ public CharMap () return true; }); AddCommand (Command.PageUp, () => { - var page = (Bounds.Height / _rowHeight - 1) * 16; + int page = (Bounds.Height / _rowHeight - 1) * 16; SelectedCodePoint -= Math.Min (page, SelectedCodePoint); return true; }); AddCommand (Command.PageDown, () => { - var page = (Bounds.Height / _rowHeight - 1) * 16; + int page = (Bounds.Height / _rowHeight - 1) * 16; SelectedCodePoint += Math.Min (page, MaxCodePoint - SelectedCodePoint); return true; }); @@ -370,7 +375,7 @@ public CharMap () SelectedCodePoint = MaxCodePoint; return true; }); - AddKeyBinding (Key.Enter, Command.Accept); + KeyBindings.Add (Key.Enter, Command.Accept); AddCommand (Command.Accept, () => { ShowDetails (); return true; @@ -379,11 +384,10 @@ public CharMap () MouseClick += Handle_MouseClick; } - private void CopyCodePoint () => Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; - private void CopyGlyph () => Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; + void CopyCodePoint () => Clipboard.Contents = $"U+{SelectedCodePoint:x5}"; + void CopyGlyph () => Clipboard.Contents = $"{new Rune (SelectedCodePoint)}"; - public override void OnDrawContent (Rect contentArea) - { + public override void OnDrawContent (Rect contentArea) => //if (ShowHorizontalScrollIndicator && ContentSize.Height < (int)(MaxCodePoint / 16 + 2)) { // //ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePoint / 16 + 2)); // //ContentSize = new Size (CharMap.RowWidth, (int)(MaxCodePoint / 16) * _rowHeight + 2); @@ -404,7 +408,6 @@ public override void OnDrawContent (Rect contentArea) // ContentOffset = new Point (0, ContentOffset.Y < -ContentSize.Height + Bounds.Height ? ContentOffset.Y - 1 : ContentOffset.Y); //} base.OnDrawContent (contentArea); - } //public void CharMap_DrawContent (object s, DrawEventArgs a) public override void OnDrawContentComplete (Rect contentArea) @@ -412,7 +415,7 @@ public override void OnDrawContentComplete (Rect contentArea) if (contentArea.Height == 0 || contentArea.Width == 0) { return; } - Rect viewport = new Rect (ContentOffset, + var viewport = new Rect (ContentOffset, new Size (Math.Max (Bounds.Width - (ShowVerticalScrollIndicator ? 1 : 0), 0), Math.Max (Bounds.Height - (ShowHorizontalScrollIndicator ? 1 : 0), 0))); @@ -426,31 +429,31 @@ public override void OnDrawContentComplete (Rect contentArea) Driver.Clip = new Rect (Driver.Clip.Location, new Size (Driver.Clip.Width - 1, Driver.Clip.Height)); } - var cursorCol = Cursor.X - ContentOffset.X - RowLabelWidth - 1; - var cursorRow = Cursor.Y - ContentOffset.Y - 1; + int cursorCol = Cursor.X - ContentOffset.X - RowLabelWidth - 1; + int cursorRow = Cursor.Y - ContentOffset.Y - 1; Driver.SetAttribute (GetHotNormalColor ()); Move (0, 0); Driver.AddStr (new string (' ', RowLabelWidth + 1)); for (int hexDigit = 0; hexDigit < 16; hexDigit++) { - var x = ContentOffset.X + RowLabelWidth + (hexDigit * COLUMN_WIDTH); + int x = ContentOffset.X + RowLabelWidth + hexDigit * COLUMN_WIDTH; if (x > RowLabelWidth - 2) { Move (x, 0); Driver.SetAttribute (GetHotNormalColor ()); Driver.AddStr (" "); - Driver.SetAttribute (HasFocus && (cursorCol + ContentOffset.X + RowLabelWidth == x) ? ColorScheme.HotFocus : GetHotNormalColor ()); + Driver.SetAttribute (HasFocus && cursorCol + ContentOffset.X + RowLabelWidth == x ? ColorScheme.HotFocus : GetHotNormalColor ()); Driver.AddStr ($"{hexDigit:x}"); Driver.SetAttribute (GetHotNormalColor ()); Driver.AddStr (" "); } } - var firstColumnX = viewport.X + RowLabelWidth; + int firstColumnX = viewport.X + RowLabelWidth; for (int y = 1; y < Bounds.Height; y++) { // What row is this? - var row = (y - ContentOffset.Y - 1) / _rowHeight; + int row = (y - ContentOffset.Y - 1) / _rowHeight; - var val = (row) * 16; + int val = row * 16; if (val > MaxCodePoint) { continue; } @@ -458,18 +461,18 @@ public override void OnDrawContentComplete (Rect contentArea) Driver.SetAttribute (GetNormalColor ()); for (int col = 0; col < 16; col++) { - var x = firstColumnX + COLUMN_WIDTH * col + 1; + int x = firstColumnX + COLUMN_WIDTH * col + 1; Move (x, y); if (cursorRow + ContentOffset.Y + 1 == y && cursorCol + ContentOffset.X + firstColumnX + 1 == x && !HasFocus) { Driver.SetAttribute (GetFocusColor ()); } - var scalar = val + col; - Rune rune = (Rune)'?'; + int scalar = val + col; + var rune = (Rune)'?'; if (Rune.IsValid (scalar)) { rune = new Rune (scalar); } - var width = rune.GetColumns (); + int width = rune.GetColumns (); // are we at first row of the row? if (!ShowGlyphWidths || (y - ContentOffset.Y) % _rowHeight > 0) { @@ -487,7 +490,7 @@ public override void OnDrawContentComplete (Rect contentArea) sb.Append (rune); // Try normalizing after combining with 'a'. If it normalizes, at least // it'll show on the 'a'. If not, just show the replacement char. - var normal = sb.ToString ().Normalize (NormalizationForm.FormC); + string normal = sb.ToString ().Normalize (NormalizationForm.FormC); if (normal.Length == 1) { Driver.AddRune (normal [0]); } else { @@ -505,7 +508,7 @@ public override void OnDrawContentComplete (Rect contentArea) } } Move (0, y); - Driver.SetAttribute (HasFocus && (cursorRow + ContentOffset.Y + 1 == y) ? ColorScheme.HotFocus : ColorScheme.HotNormal); + Driver.SetAttribute (HasFocus && cursorRow + ContentOffset.Y + 1 == y ? ColorScheme.HotFocus : ColorScheme.HotNormal); if (!ShowGlyphWidths || (y - ContentOffset.Y) % _rowHeight > 0) { Driver.AddStr ($"U+{val / 16:x5}_ "); } else { @@ -515,12 +518,13 @@ public override void OnDrawContentComplete (Rect contentArea) Driver.Clip = oldClip; } - ContextMenu _contextMenu = new ContextMenu (); + ContextMenu _contextMenu = new (); + void Handle_MouseClick (object sender, MouseEventEventArgs args) { var me = args.MouseEvent; if (me.Flags != MouseFlags.ReportMousePosition && me.Flags != MouseFlags.Button1Clicked && - me.Flags != MouseFlags.Button1DoubleClicked) { + me.Flags != MouseFlags.Button1DoubleClicked) { return; } @@ -528,21 +532,20 @@ void Handle_MouseClick (object sender, MouseEventEventArgs args) me.Y = Cursor.Y; } - if (me.Y > 0) { - } + if (me.Y > 0) { } - if (me.X < RowLabelWidth || me.X > RowLabelWidth + (16 * COLUMN_WIDTH) - 1) { + if (me.X < RowLabelWidth || me.X > RowLabelWidth + 16 * COLUMN_WIDTH - 1) { me.X = Cursor.X; } - var row = (me.Y - 1 - ContentOffset.Y) / _rowHeight; // -1 for header - var col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH; + int row = (me.Y - 1 - ContentOffset.Y) / _rowHeight; // -1 for header + int col = (me.X - RowLabelWidth - ContentOffset.X) / COLUMN_WIDTH; if (col > 15) { col = 15; } - var val = (row) * 16 + col; + int val = row * 16 + col; if (val > MaxCodePoint) { return; } @@ -566,11 +569,9 @@ void Handle_MouseClick (object sender, MouseEventEventArgs args) SelectedCodePoint = val; _contextMenu = new ContextMenu (me.X + 1, me.Y + 1, new MenuBarItem (new MenuItem [] { - new MenuItem ("_Copy Glyph", "", () => CopyGlyph (), null, null, Key.C | Key.CtrlMask), - new MenuItem ("Copy Code _Point", "", () => CopyCodePoint (), null, null, Key.C | Key.ShiftMask | Key.CtrlMask), - }) { - - } + new ("_Copy Glyph", "", () => CopyGlyph (), null, null, (KeyCode)Key.C.WithCtrl), + new ("Copy Code _Point", "", () => CopyCodePoint (), null, null, (KeyCode)Key.C.WithCtrl.WithShift) + }) { } ); _contextMenu.Show (); } @@ -582,7 +583,7 @@ public static string ToCamelCase (string str) return str; } - TextInfo textInfo = new CultureInfo ("en-US", false).TextInfo; + var textInfo = new CultureInfo ("en-US", false).TextInfo; str = textInfo.ToLower (str); str = textInfo.ToTitleCase (str); @@ -614,7 +615,7 @@ void ShowDetails () var spinner = new SpinnerView () { X = Pos.Center (), Y = Pos.Center (), - Style = new SpinnerStyle.Aesthetic (), + Style = new Aesthetic () }; spinner.AutoSpin = true; @@ -640,11 +641,11 @@ void ShowDetails () if (!string.IsNullOrEmpty (decResponse)) { string name = string.Empty; - using (JsonDocument document = JsonDocument.Parse (decResponse)) { - JsonElement root = document.RootElement; + using (var document = JsonDocument.Parse (decResponse)) { + var root = document.RootElement; // Get a property by name and output its value - if (root.TryGetProperty ("name", out JsonElement nameElement)) { + if (root.TryGetProperty ("name", out var nameElement)) { name = nameElement.GetString (); } @@ -654,12 +655,12 @@ void ShowDetails () // Console.WriteLine (nestedPropertyElement.GetString ()); //} decResponse = JsonSerializer.Serialize (document.RootElement, new - JsonSerializerOptions { - WriteIndented = true - }); + JsonSerializerOptions { + WriteIndented = true + }); } - var title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}"; + string title = $"{ToCamelCase (name)} - {new Rune (SelectedCodePoint)} U+{SelectedCodePoint:x5}"; var copyGlyph = new Button ("Copy _Glyph"); var copyCP = new Button ("Copy Code _Point"); @@ -819,7 +820,7 @@ public override bool OnLeave (View view) } public class UcdApiClient { - private static readonly HttpClient httpClient = new HttpClient (); + static readonly HttpClient httpClient = new (); public const string BaseUrl = "https://ucdapi.org/unicode/latest/"; public async Task GetCodepointHex (string hex) @@ -851,49 +852,49 @@ public async Task GetCharsName (string chars) } } - class UnicodeRange { public int Start; public int End; public string Category; + public UnicodeRange (int start, int end, string category) { - this.Start = start; - this.End = end; - this.Category = category; + Start = start; + End = end; + Category = category; } public static List GetRanges () { - var ranges = (from r in typeof (UnicodeRanges).GetProperties (System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) - let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange - let name = string.IsNullOrEmpty (r.Name) ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}" : r.Name - where name != "None" && name != "All" - select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name)); + var ranges = from r in typeof (UnicodeRanges).GetProperties (System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public) + let urange = r.GetValue (null) as System.Text.Unicode.UnicodeRange + let name = string.IsNullOrEmpty (r.Name) ? $"U+{urange.FirstCodePoint:x5}-U+{urange.FirstCodePoint + urange.Length:x5}" : r.Name + where name != "None" && name != "All" + select new UnicodeRange (urange.FirstCodePoint, urange.FirstCodePoint + urange.Length, name); // .NET 8.0 only supports BMP in UnicodeRanges: https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.unicoderanges?view=net-8.0 var nonBmpRanges = new List { - new UnicodeRange (0x1F130, 0x1F149 ,"Squared Latin Capital Letters"), - new UnicodeRange (0x12400, 0x1240f ,"Cuneiform Numbers and Punctuation"), - new UnicodeRange (0x10000, 0x1007F ,"Linear B Syllabary"), - new UnicodeRange (0x10080, 0x100FF ,"Linear B Ideograms"), - new UnicodeRange (0x10100, 0x1013F ,"Aegean Numbers"), - new UnicodeRange (0x10300, 0x1032F ,"Old Italic"), - new UnicodeRange (0x10330, 0x1034F ,"Gothic"), - new UnicodeRange (0x10380, 0x1039F ,"Ugaritic"), - new UnicodeRange (0x10400, 0x1044F ,"Deseret"), - new UnicodeRange (0x10450, 0x1047F ,"Shavian"), - new UnicodeRange (0x10480, 0x104AF ,"Osmanya"), - new UnicodeRange (0x10800, 0x1083F ,"Cypriot Syllabary"), - new UnicodeRange (0x1D000, 0x1D0FF ,"Byzantine Musical Symbols"), - new UnicodeRange (0x1D100, 0x1D1FF ,"Musical Symbols"), - new UnicodeRange (0x1D300, 0x1D35F ,"Tai Xuan Jing Symbols"), - new UnicodeRange (0x1D400, 0x1D7FF ,"Mathematical Alphanumeric Symbols"), - new UnicodeRange (0x1F600, 0x1F532 ,"Emojis Symbols"), - new UnicodeRange (0x20000, 0x2A6DF ,"CJK Unified Ideographs Extension B"), - new UnicodeRange (0x2F800, 0x2FA1F ,"CJK Compatibility Ideographs Supplement"), - new UnicodeRange (0xE0000, 0xE007F ,"Tags"), + new (0x1F130, 0x1F149, "Squared Latin Capital Letters"), + new (0x12400, 0x1240f, "Cuneiform Numbers and Punctuation"), + new (0x10000, 0x1007F, "Linear B Syllabary"), + new (0x10080, 0x100FF, "Linear B Ideograms"), + new (0x10100, 0x1013F, "Aegean Numbers"), + new (0x10300, 0x1032F, "Old Italic"), + new (0x10330, 0x1034F, "Gothic"), + new (0x10380, 0x1039F, "Ugaritic"), + new (0x10400, 0x1044F, "Deseret"), + new (0x10450, 0x1047F, "Shavian"), + new (0x10480, 0x104AF, "Osmanya"), + new (0x10800, 0x1083F, "Cypriot Syllabary"), + new (0x1D000, 0x1D0FF, "Byzantine Musical Symbols"), + new (0x1D100, 0x1D1FF, "Musical Symbols"), + new (0x1D300, 0x1D35F, "Tai Xuan Jing Symbols"), + new (0x1D400, 0x1D7FF, "Mathematical Alphanumeric Symbols"), + new (0x1F600, 0x1F532, "Emojis Symbols"), + new (0x20000, 0x2A6DF, "CJK Unified Ideographs Extension B"), + new (0x2F800, 0x2FA1F, "CJK Compatibility Ideographs Supplement"), + new (0xE0000, 0xE007F, "Tags") }; return ranges.Concat (nonBmpRanges).OrderBy (r => r.Category).ToList (); diff --git a/UICatalog/Scenarios/CollectionNavigatorTester.cs b/UICatalog/Scenarios/CollectionNavigatorTester.cs index 993e993b8e..ce1a8444f7 100644 --- a/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -95,7 +95,7 @@ public override void Setup () allowMarking, allowMultiSelection, null, - new MenuItem ("_Quit", $"{Application.QuitKey}", () => Quit(), null, null, Application.QuitKey), + new MenuItem ("_Quit", $"{Application.QuitKey}", () => Quit(), null, null, (KeyCode)Application.QuitKey), }), new MenuBarItem("_Quit", $"{Application.QuitKey}", () => Quit()), }); diff --git a/UICatalog/Scenarios/ConfigurationEditor.cs b/UICatalog/Scenarios/ConfigurationEditor.cs index 9ff0115b83..2cc7a73270 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -49,11 +49,11 @@ public override void Setup () Application.Top.Add (_tileView); - _lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null); + _lenStatusItem = new StatusItem (KeyCode.CharMask, "Len: ", null); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} Quit", () => Quit()), - new StatusItem(Key.F5, "~F5~ Reload", () => Reload()), - new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), + new StatusItem(KeyCode.F5, "~F5~ Reload", () => Reload()), + new StatusItem(KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save()), _lenStatusItem, }); diff --git a/UICatalog/Scenarios/ContextMenus.cs b/UICatalog/Scenarios/ContextMenus.cs index c39cb248c6..49ed6ec8cb 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -3,97 +3,104 @@ using System.Threading; using Terminal.Gui; -namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "ContextMenus", Description: "Context Menu Sample.")] - [ScenarioCategory ("Menus")] - public class ContextMenus : Scenario { - private ContextMenu _contextMenu = new ContextMenu (); - private readonly List _cultureInfos = Application.SupportedCultures; - private MenuItem _miForceMinimumPosToZero; - private bool _forceMinimumPosToZero = true; - private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; - private MenuItem _miUseSubMenusSingleFrame; - private bool _useSubMenusSingleFrame; - - public override void Setup () - { - var text = "Context Menu"; - var width = 20; - - Win.Add (new Label ("Press 'Ctrl + Space' to open the Window context menu.") { - X = Pos.Center (), - Y = 1 - }); - - _tfTopLeft = new TextField (text) { - Width = width - }; - Win.Add (_tfTopLeft); - - _tfTopRight = new TextField (text) { - X = Pos.AnchorEnd (width), - Width = width - }; - Win.Add (_tfTopRight); - - _tfMiddle = new TextField (text) { - X = Pos.Center (), - Y = Pos.Center (), - Width = width - }; - Win.Add (_tfMiddle); - - _tfBottomLeft = new TextField (text) { - Y = Pos.AnchorEnd (1), - Width = width - }; - Win.Add (_tfBottomLeft); - - _tfBottomRight = new TextField (text) { - X = Pos.AnchorEnd (width), - Y = Pos.AnchorEnd (1), - Width = width - }; - Win.Add (_tfBottomRight); - - Point mousePos = default; - - Win.KeyPressed += (s, e) => { - if (e.KeyEvent.Key == (Key.Space | Key.CtrlMask)) { - ShowContextMenu (mousePos.X, mousePos.Y); - e.Handled = true; - } - }; - - Win.MouseClick += (s, e) => { - if (e.MouseEvent.Flags == _contextMenu.MouseFlags) { - ShowContextMenu (e.MouseEvent.X, e.MouseEvent.Y); - e.Handled = true; - } - }; - - Application.MouseEvent += ApplicationMouseEvent; +namespace UICatalog.Scenarios; +[ScenarioMetadata (Name: "ContextMenus", Description: "Context Menu Sample.")] +[ScenarioCategory ("Menus")] +public class ContextMenus : Scenario { + private ContextMenu _contextMenu = new ContextMenu (); + private readonly List _cultureInfos = Application.SupportedCultures; + private MenuItem _miForceMinimumPosToZero; + private bool _forceMinimumPosToZero = true; + private TextField _tfTopLeft, _tfTopRight, _tfMiddle, _tfBottomLeft, _tfBottomRight; + private MenuItem _miUseSubMenusSingleFrame; + private bool _useSubMenusSingleFrame; + + public override void Setup () + { + var text = "Context Menu"; + var width = 20; + KeyCode winContextMenuKey = KeyCode.Space | KeyCode.CtrlMask; + + var label = new Label ($"Press '{winContextMenuKey}' to open the Window context menu.") { + X = Pos.Center (), + Y = 1 + }; + Win.Add (label); + label = new Label ($"Press '{ContextMenu.DefaultKey}' to open the TextField context menu.") { + X = Pos.Center (), + Y = Pos.Bottom (label) + }; + Win.Add (label); + + _tfTopLeft = new TextField (text) { + Width = width + }; + Win.Add (_tfTopLeft); + + _tfTopRight = new TextField (text) { + X = Pos.AnchorEnd (width), + Width = width + }; + Win.Add (_tfTopRight); + + _tfMiddle = new TextField (text) { + X = Pos.Center (), + Y = Pos.Center (), + Width = width + }; + Win.Add (_tfMiddle); + + _tfBottomLeft = new TextField (text) { + Y = Pos.AnchorEnd (1), + Width = width + }; + Win.Add (_tfBottomLeft); + + _tfBottomRight = new TextField (text) { + X = Pos.AnchorEnd (width), + Y = Pos.AnchorEnd (1), + Width = width + }; + Win.Add (_tfBottomRight); + + Point mousePos = default; + + Win.KeyDown += (s, e) => { + if (e.KeyCode == winContextMenuKey) { + ShowContextMenu (mousePos.X, mousePos.Y); + e.Handled = true; + } + }; - void ApplicationMouseEvent (object sender, MouseEventEventArgs a) - { - mousePos = new Point (a.MouseEvent.X, a.MouseEvent.Y); + Win.MouseClick += (s, e) => { + if (e.MouseEvent.Flags == _contextMenu.MouseFlags) { + ShowContextMenu (e.MouseEvent.X, e.MouseEvent.Y); + e.Handled = true; } + }; - Win.WantMousePositionReports = true; + Application.MouseEvent += ApplicationMouseEvent; - Application.Top.Closed += (s,e) => { - Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); - Application.MouseEvent -= ApplicationMouseEvent; - }; + void ApplicationMouseEvent (object sender, MouseEventEventArgs a) + { + mousePos = new Point (a.MouseEvent.X, a.MouseEvent.Y); } - private void ShowContextMenu (int x, int y) - { - _contextMenu = new ContextMenu (x, y, - new MenuBarItem (new MenuItem [] { + Win.WantMousePositionReports = true; + + Application.Top.Closed += (s, e) => { + Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); + Application.MouseEvent -= ApplicationMouseEvent; + }; + } + + private void ShowContextMenu (int x, int y) + { + _contextMenu = new ContextMenu (x, y, + new MenuBarItem (new MenuItem [] { new MenuItem ("_Configuration", "Show configuration", () => MessageBox.Query (50, 5, "Info", "This would open settings dialog", "Ok")), new MenuBarItem ("More options", new MenuItem [] { - new MenuItem ("_Setup", "Change settings", () => MessageBox.Query (50, 5, "Info", "This would open setup dialog", "Ok")), + new MenuItem ("_Setup", "Change settings", () => MessageBox.Query (50, 5, "Info", "This would open setup dialog", "Ok"), shortcut: KeyCode.T | KeyCode.CtrlMask), new MenuItem ("_Maintenance", "Maintenance mode", () => MessageBox.Query (50, 5, "Info", "This would open maintenance dialog", "Ok")), }), new MenuBarItem ("_Languages", GetSupportedCultures ()), @@ -111,56 +118,55 @@ private void ShowContextMenu (int x, int y) }, null, new MenuItem ("_Quit", "", () => Application.RequestStop ()) - }) - ) { ForceMinimumPosToZero = _forceMinimumPosToZero, UseSubMenusSingleFrame = _useSubMenusSingleFrame }; + }) + ) { ForceMinimumPosToZero = _forceMinimumPosToZero, UseSubMenusSingleFrame = _useSubMenusSingleFrame }; - _tfTopLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfTopRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfMiddle.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfBottomLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _tfBottomRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + _tfTopLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + _tfTopRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + _tfMiddle.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + _tfBottomLeft.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; + _tfBottomRight.ContextMenu.ForceMinimumPosToZero = _forceMinimumPosToZero; - _contextMenu.Show (); - } + _contextMenu.Show (); + } - private MenuItem [] GetSupportedCultures () - { - List supportedCultures = new List (); - var index = -1; + private MenuItem [] GetSupportedCultures () + { + List supportedCultures = new List (); + var index = -1; - foreach (var c in _cultureInfos) { - var culture = new MenuItem { - CheckType = MenuItemCheckStyle.Checked - }; - if (index == -1) { - culture.Title = "_English"; - culture.Help = "en-US"; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == "en-US"; - CreateAction (supportedCultures, culture); - supportedCultures.Add (culture); - index++; - culture = new MenuItem { - CheckType = MenuItemCheckStyle.Checked - }; - } - culture.Title = $"_{c.Parent.EnglishName}"; - culture.Help = c.Name; - culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == c.Name; + foreach (var c in _cultureInfos) { + var culture = new MenuItem { + CheckType = MenuItemCheckStyle.Checked + }; + if (index == -1) { + culture.Title = "_English"; + culture.Help = "en-US"; + culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == "en-US"; CreateAction (supportedCultures, culture); supportedCultures.Add (culture); - } - return supportedCultures.ToArray (); - - void CreateAction (List supportedCultures, MenuItem culture) - { - culture.Action += () => { - Thread.CurrentThread.CurrentUICulture = new CultureInfo (culture.Help); - culture.Checked = true; - foreach (var item in supportedCultures) { - item.Checked = item.Help == Thread.CurrentThread.CurrentUICulture.Name; - } + index++; + culture = new MenuItem { + CheckType = MenuItemCheckStyle.Checked }; } + culture.Title = $"_{c.Parent.EnglishName}"; + culture.Help = c.Name; + culture.Checked = Thread.CurrentThread.CurrentUICulture.Name == c.Name; + CreateAction (supportedCultures, culture); + supportedCultures.Add (culture); + } + return supportedCultures.ToArray (); + + void CreateAction (List supportedCultures, MenuItem culture) + { + culture.Action += () => { + Thread.CurrentThread.CurrentUICulture = new CultureInfo (culture.Help); + culture.Checked = true; + foreach (var item in supportedCultures) { + item.Checked = item.Help == Thread.CurrentThread.CurrentUICulture.Name; + } + }; } } -} \ No newline at end of file +} diff --git a/UICatalog/Scenarios/CsvEditor.cs b/UICatalog/Scenarios/CsvEditor.cs index 96ff667ffe..57bb099ab8 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -67,8 +67,8 @@ public override void Setup () Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { - new StatusItem(Key.CtrlMask | Key.O, "~^O~ Open", () => Open()), - new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), + new StatusItem(KeyCode.CtrlMask | KeyCode.O, "~^O~ Open", () => Open()), + new StatusItem(KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save()), new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), }); Application.Top.Add (statusBar); @@ -88,7 +88,7 @@ public override void Setup () tableView.SelectedCellChanged += OnSelectedCellChanged; tableView.CellActivated += EditCurrentCell; - tableView.KeyPressed += TableViewKeyPress; + tableView.KeyDown += TableViewKeyPress; SetupScrollBar (); } @@ -465,9 +465,9 @@ private void SetupScrollBar () } - private void TableViewKeyPress (object sender, KeyEventEventArgs e) + private void TableViewKeyPress (object sender, Key e) { - if (e.KeyEvent.Key == Key.DeleteChar) { + if (e.KeyCode == KeyCode.DeleteChar) { if (tableView.FullRowSelect) { // Delete button deletes all rows when in full row mode diff --git a/UICatalog/Scenarios/DynamicMenuBar.cs b/UICatalog/Scenarios/DynamicMenuBar.cs index 62a8fcb80d..657cc8dc9c 100644 --- a/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/UICatalog/Scenarios/DynamicMenuBar.cs @@ -84,11 +84,11 @@ public DynamicMenuBarSample () : base () Height = 4 }; - var _txtDelimiter = new TextField (MenuBar.ShortcutDelimiter) { + var _txtDelimiter = new TextField (MenuBar.ShortcutDelimiter.ToString()) { X = Pos.Center (), Width = 2, }; - _txtDelimiter.TextChanged += (s, _) => MenuBar.ShortcutDelimiter = _txtDelimiter.Text; + _txtDelimiter.TextChanged += (s, _) => MenuBar.ShortcutDelimiter = _txtDelimiter.Text.ToRunes()[0]; _frmDelimiter.Add (_txtDelimiter); Add (_frmDelimiter); @@ -723,30 +723,28 @@ public DynamicMenuBarDetails (string title) : base (title) ReadOnly = true }; _txtShortcut.KeyDown += (s, e) => { - if (!ProcessKey (e.KeyEvent)) { + if (!ProcessKey (e)) { return; } - - var k = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (CheckShortcut (k, true)) { + if (CheckShortcut (e.KeyCode, true)) { e.Handled = true; } }; - bool ProcessKey (KeyEvent ev) + bool ProcessKey (Key ev) { - switch (ev.Key) { - case Key.CursorUp: - case Key.CursorDown: - case Key.Tab: - case Key.BackTab: + switch (ev.KeyCode) { + case KeyCode.CursorUp: + case KeyCode.CursorDown: + case KeyCode.Tab: + case KeyCode.Tab | KeyCode.ShiftMask: return false; } return true; } - bool CheckShortcut (Key k, bool pre) + bool CheckShortcut (KeyCode k, bool pre) { var m = _menuItem != null ? _menuItem : new MenuItem (); if (pre && !ShortcutHelper.PreShortcutValidation (k)) { @@ -760,14 +758,13 @@ bool CheckShortcut (Key k, bool pre) } return true; } - _txtShortcut.Text = ShortcutHelper.GetShortcutTag (k); + _txtShortcut.Text = Key.ToString (k, MenuBar.ShortcutDelimiter);// ShortcutHelper.GetShortcutTag (k); return true; } _txtShortcut.KeyUp += (s, e) => { - var k = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (CheckShortcut (k, false)) { + if (CheckShortcut (e.KeyCode, false)) { e.Handled = true; } }; diff --git a/UICatalog/Scenarios/DynamicStatusBar.cs b/UICatalog/Scenarios/DynamicStatusBar.cs index e28092117a..1acd55b0e2 100644 --- a/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/UICatalog/Scenarios/DynamicStatusBar.cs @@ -7,652 +7,649 @@ using System.Runtime.CompilerServices; using Terminal.Gui; -namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Dynamic StatusBar", Description: "Demonstrates how to add and remove a StatusBar and change items dynamically.")] - [ScenarioCategory ("Top Level Windows")] - public class DynamicStatusBar : Scenario { - public override void Init () - { - Application.Init (); - Application.Top.Add (new DynamicStatusBarSample () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }); - } +namespace UICatalog.Scenarios; +[ScenarioMetadata (Name: "Dynamic StatusBar", Description: "Demonstrates how to add and remove a StatusBar and change items dynamically.")] +[ScenarioCategory ("Top Level Windows")] +public class DynamicStatusBar : Scenario { + public override void Init () + { + Application.Init (); + Application.Top.Add (new DynamicStatusBarSample () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }); + } - public class DynamicStatusItemList { - public string Title { get; set; } - public StatusItem StatusItem { get; set; } + public class DynamicStatusItemList { + public string Title { get; set; } + public StatusItem StatusItem { get; set; } - public DynamicStatusItemList () { } + public DynamicStatusItemList () { } - public DynamicStatusItemList (string title, StatusItem statusItem) - { - Title = title; - StatusItem = statusItem; - } - - public override string ToString () => $"{Title}, {StatusItem}"; + public DynamicStatusItemList (string title, StatusItem statusItem) + { + Title = title; + StatusItem = statusItem; } - public class DynamicStatusItem { - public string title = "New"; - public string action = ""; - public string shortcut; + public override string ToString () => $"{Title}, {StatusItem}"; + } - public DynamicStatusItem () { } + public class DynamicStatusItem { + public string title = "New"; + public string action = ""; + public string shortcut; - public DynamicStatusItem (string title) - { - this.title = title; - } + public DynamicStatusItem () { } - public DynamicStatusItem (string title, string action, string shortcut = null) - { - this.title = title; - this.action = action; - this.shortcut = shortcut; - } + public DynamicStatusItem (string title) + { + this.title = title; } - public class DynamicStatusBarSample : Window { - StatusBar _statusBar; - StatusItem _currentStatusItem; - int _currentSelectedStatusBar = -1; - StatusItem _currentEditStatusItem; - ListView _lstItems; - - public DynamicStatusItemModel DataContext { get; set; } - - public DynamicStatusBarSample () : base () - { - DataContext = new DynamicStatusItemModel (); - - var _frmDelimiter = new FrameView ("Shortcut Delimiter:") { - X = Pos.Center (), - Y = 0, - Width = 25, - Height = 4 - }; - - var _txtDelimiter = new TextField (StatusBar.ShortcutDelimiter) { - X = Pos.Center (), - Width = 2, - }; - _txtDelimiter.TextChanged += (s, _) => StatusBar.ShortcutDelimiter = _txtDelimiter.Text; - _frmDelimiter.Add (_txtDelimiter); - - Add (_frmDelimiter); - - var _frmStatusBar = new FrameView ("Items:") { - Y = 5, - Width = Dim.Percent (50), - Height = Dim.Fill (2) - }; - - var _btnAddStatusBar = new Button ("Add a StatusBar") { - Y = 1, - }; - _frmStatusBar.Add (_btnAddStatusBar); - - var _btnRemoveStatusBar = new Button ("Remove a StatusBar") { - Y = 1 - }; - _btnRemoveStatusBar.X = Pos.AnchorEnd () - (Pos.Right (_btnRemoveStatusBar) - Pos.Left (_btnRemoveStatusBar)); - _frmStatusBar.Add (_btnRemoveStatusBar); - - var _btnAdd = new Button (" Add ") { - Y = Pos.Top (_btnRemoveStatusBar) + 2, - }; - _btnAdd.X = Pos.AnchorEnd () - (Pos.Right (_btnAdd) - Pos.Left (_btnAdd)); - _frmStatusBar.Add (_btnAdd); - - _lstItems = new ListView (new List ()) { - ColorScheme = Colors.Dialog, - Y = Pos.Top (_btnAddStatusBar) + 2, - Width = Dim.Fill () - Dim.Width (_btnAdd) - 1, - Height = Dim.Fill (), - }; - _frmStatusBar.Add (_lstItems); - - var _btnRemove = new Button ("Remove") { - X = Pos.Left (_btnAdd), - Y = Pos.Top (_btnAdd) + 1 - }; - _frmStatusBar.Add (_btnRemove); - - var _btnUp = new Button ("^") { - X = Pos.Right (_lstItems) + 2, - Y = Pos.Top (_btnRemove) + 2 - }; - _frmStatusBar.Add (_btnUp); + public DynamicStatusItem (string title, string action, string shortcut = null) + { + this.title = title; + this.action = action; + this.shortcut = shortcut; + } + } - var _btnDown = new Button ("v") { - X = Pos.Right (_lstItems) + 2, - Y = Pos.Top (_btnUp) + 1 - }; - _frmStatusBar.Add (_btnDown); + public class DynamicStatusBarSample : Window { + StatusBar _statusBar; + StatusItem _currentStatusItem; + int _currentSelectedStatusBar = -1; + StatusItem _currentEditStatusItem; + ListView _lstItems; - Add (_frmStatusBar); + public DynamicStatusItemModel DataContext { get; set; } - var _frmStatusBarDetails = new DynamicStatusBarDetails ("StatusBar Item Details:") { - X = Pos.Right (_frmStatusBar), - Y = Pos.Top (_frmStatusBar), - Width = Dim.Fill (), - Height = Dim.Fill (4) - }; - Add (_frmStatusBarDetails); - - _btnUp.Clicked += (s,e) => { - var i = _lstItems.SelectedItem; - var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; - if (statusItem != null) { - var items = _statusBar.Items; - if (i > 0) { - items [i] = items [i - 1]; - items [i - 1] = statusItem; - DataContext.Items [i] = DataContext.Items [i - 1]; - DataContext.Items [i - 1] = new DynamicStatusItemList (statusItem.Title, statusItem); - _lstItems.SelectedItem = i - 1; - _statusBar.SetNeedsDisplay (); - } + public DynamicStatusBarSample () : base () + { + DataContext = new DynamicStatusItemModel (); + + var _frmDelimiter = new FrameView ("Shortcut Delimiter:") { + X = Pos.Center (), + Y = 0, + Width = 25, + Height = 4 + }; + + var _txtDelimiter = new TextField ($"{StatusBar.ShortcutDelimiter}") { + X = Pos.Center (), + Width = 2, + }; + _txtDelimiter.TextChanged += (s, _) => StatusBar.ShortcutDelimiter = _txtDelimiter.Text.ToRunes () [0]; + _frmDelimiter.Add (_txtDelimiter); + + Add (_frmDelimiter); + + var _frmStatusBar = new FrameView ("Items:") { + Y = 5, + Width = Dim.Percent (50), + Height = Dim.Fill (2) + }; + + var _btnAddStatusBar = new Button ("Add a StatusBar") { + Y = 1, + }; + _frmStatusBar.Add (_btnAddStatusBar); + + var _btnRemoveStatusBar = new Button ("Remove a StatusBar") { + Y = 1 + }; + _btnRemoveStatusBar.X = Pos.AnchorEnd () - (Pos.Right (_btnRemoveStatusBar) - Pos.Left (_btnRemoveStatusBar)); + _frmStatusBar.Add (_btnRemoveStatusBar); + + var _btnAdd = new Button (" Add ") { + Y = Pos.Top (_btnRemoveStatusBar) + 2, + }; + _btnAdd.X = Pos.AnchorEnd () - (Pos.Right (_btnAdd) - Pos.Left (_btnAdd)); + _frmStatusBar.Add (_btnAdd); + + _lstItems = new ListView (new List ()) { + ColorScheme = Colors.Dialog, + Y = Pos.Top (_btnAddStatusBar) + 2, + Width = Dim.Fill () - Dim.Width (_btnAdd) - 1, + Height = Dim.Fill (), + }; + _frmStatusBar.Add (_lstItems); + + var _btnRemove = new Button ("Remove") { + X = Pos.Left (_btnAdd), + Y = Pos.Top (_btnAdd) + 1 + }; + _frmStatusBar.Add (_btnRemove); + + var _btnUp = new Button ("^") { + X = Pos.Right (_lstItems) + 2, + Y = Pos.Top (_btnRemove) + 2 + }; + _frmStatusBar.Add (_btnUp); + + var _btnDown = new Button ("v") { + X = Pos.Right (_lstItems) + 2, + Y = Pos.Top (_btnUp) + 1 + }; + _frmStatusBar.Add (_btnDown); + + Add (_frmStatusBar); + + var _frmStatusBarDetails = new DynamicStatusBarDetails ("StatusBar Item Details:") { + X = Pos.Right (_frmStatusBar), + Y = Pos.Top (_frmStatusBar), + Width = Dim.Fill (), + Height = Dim.Fill (4) + }; + Add (_frmStatusBarDetails); + + _btnUp.Clicked += (s, e) => { + var i = _lstItems.SelectedItem; + var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; + if (statusItem != null) { + var items = _statusBar.Items; + if (i > 0) { + items [i] = items [i - 1]; + items [i - 1] = statusItem; + DataContext.Items [i] = DataContext.Items [i - 1]; + DataContext.Items [i - 1] = new DynamicStatusItemList (statusItem.Title, statusItem); + _lstItems.SelectedItem = i - 1; + _statusBar.SetNeedsDisplay (); } - }; - - _btnDown.Clicked += (s,e) => { - var i = _lstItems.SelectedItem; - var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; - if (statusItem != null) { - var items = _statusBar.Items; - if (i < items.Length - 1) { - items [i] = items [i + 1]; - items [i + 1] = statusItem; - DataContext.Items [i] = DataContext.Items [i + 1]; - DataContext.Items [i + 1] = new DynamicStatusItemList (statusItem.Title, statusItem); - _lstItems.SelectedItem = i + 1; - _statusBar.SetNeedsDisplay (); - } + } + }; + + _btnDown.Clicked += (s, e) => { + var i = _lstItems.SelectedItem; + var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [i].StatusItem : null; + if (statusItem != null) { + var items = _statusBar.Items; + if (i < items.Length - 1) { + items [i] = items [i + 1]; + items [i + 1] = statusItem; + DataContext.Items [i] = DataContext.Items [i + 1]; + DataContext.Items [i + 1] = new DynamicStatusItemList (statusItem.Title, statusItem); + _lstItems.SelectedItem = i + 1; + _statusBar.SetNeedsDisplay (); } - }; - - var _btnOk = new Button ("Ok") { - X = Pos.Right (_frmStatusBar) + 20, - Y = Pos.Bottom (_frmStatusBarDetails), - }; - Add (_btnOk); - - var _btnCancel = new Button ("Cancel") { - X = Pos.Right (_btnOk) + 3, - Y = Pos.Top (_btnOk), - }; - _btnCancel.Clicked += (s,e) => { - SetFrameDetails (_currentEditStatusItem); - }; - Add (_btnCancel); - - _lstItems.SelectedItemChanged += (s, e) => { - SetFrameDetails (); - }; + } + }; + + var _btnOk = new Button ("Ok") { + X = Pos.Right (_frmStatusBar) + 20, + Y = Pos.Bottom (_frmStatusBarDetails), + }; + Add (_btnOk); + + var _btnCancel = new Button ("Cancel") { + X = Pos.Right (_btnOk) + 3, + Y = Pos.Top (_btnOk), + }; + _btnCancel.Clicked += (s, e) => { + SetFrameDetails (_currentEditStatusItem); + }; + Add (_btnCancel); + + _lstItems.SelectedItemChanged += (s, e) => { + SetFrameDetails (); + }; + + _btnOk.Clicked += (s, e) => { + if (string.IsNullOrEmpty (_frmStatusBarDetails._txtTitle.Text) && _currentEditStatusItem != null) { + MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); + } else if (_currentEditStatusItem != null) { + _frmStatusBarDetails._txtTitle.Text = SetTitleText ( + _frmStatusBarDetails._txtTitle.Text, _frmStatusBarDetails._txtShortcut.Text); + var statusItem = new DynamicStatusItem (_frmStatusBarDetails._txtTitle.Text, + _frmStatusBarDetails._txtAction.Text, + _frmStatusBarDetails._txtShortcut.Text); + UpdateStatusItem (_currentEditStatusItem, statusItem, _lstItems.SelectedItem); + } + }; - _btnOk.Clicked += (s,e) => { - if (string.IsNullOrEmpty (_frmStatusBarDetails._txtTitle.Text) && _currentEditStatusItem != null) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); - } else if (_currentEditStatusItem != null) { - _frmStatusBarDetails._txtTitle.Text = SetTitleText ( - _frmStatusBarDetails._txtTitle.Text, _frmStatusBarDetails._txtShortcut.Text); - var statusItem = new DynamicStatusItem (_frmStatusBarDetails._txtTitle.Text, - _frmStatusBarDetails._txtAction.Text, - _frmStatusBarDetails._txtShortcut.Text); - UpdateStatusItem (_currentEditStatusItem, statusItem, _lstItems.SelectedItem); - } - }; + _btnAdd.Clicked += (s, e) => { + if (StatusBar == null) { + MessageBox.ErrorQuery ("StatusBar Bar Error", "Must add a StatusBar first!", "Ok"); + _btnAddStatusBar.SetFocus (); + return; + } - _btnAdd.Clicked += (s,e) => { - if (StatusBar == null) { - MessageBox.ErrorQuery ("StatusBar Bar Error", "Must add a StatusBar first!", "Ok"); - _btnAddStatusBar.SetFocus (); - return; - } + var frameDetails = new DynamicStatusBarDetails (); + var item = frameDetails.EnterStatusItem (); + if (item == null) { + return; + } - var frameDetails = new DynamicStatusBarDetails (); - var item = frameDetails.EnterStatusItem (); - if (item == null) { - return; + StatusItem newStatusItem = CreateNewStatusBar (item); + _currentSelectedStatusBar++; + _statusBar.AddItemAt (_currentSelectedStatusBar, newStatusItem); + DataContext.Items.Add (new DynamicStatusItemList (newStatusItem.Title, newStatusItem)); + _lstItems.MoveDown (); + SetFrameDetails (); + }; + + _btnRemove.Clicked += (s, e) => { + var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; + if (statusItem != null) { + _statusBar.RemoveItem (_currentSelectedStatusBar); + DataContext.Items.RemoveAt (_lstItems.SelectedItem); + if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1) { + _lstItems.SelectedItem = _lstItems.Source.Count - 1; } - - StatusItem newStatusItem = CreateNewStatusBar (item); - _currentSelectedStatusBar++; - _statusBar.AddItemAt (_currentSelectedStatusBar, newStatusItem); - DataContext.Items.Add (new DynamicStatusItemList (newStatusItem.Title, newStatusItem)); - _lstItems.MoveDown (); + _lstItems.SetNeedsDisplay (); SetFrameDetails (); - }; - - _btnRemove.Clicked += (s,e) => { - var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; - if (statusItem != null) { - _statusBar.RemoveItem (_currentSelectedStatusBar); - DataContext.Items.RemoveAt (_lstItems.SelectedItem); - if (_lstItems.Source.Count > 0 && _lstItems.SelectedItem > _lstItems.Source.Count - 1) { - _lstItems.SelectedItem = _lstItems.Source.Count - 1; - } - _lstItems.SetNeedsDisplay (); - SetFrameDetails (); - } - }; - - _lstItems.Enter += (s, e) => { - var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; - SetFrameDetails (statusItem); - }; + } + }; - _btnAddStatusBar.Clicked += (s,e) => { - if (_statusBar != null) { - return; - } + _lstItems.Enter += (s, e) => { + var statusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; + SetFrameDetails (statusItem); + }; - _statusBar = new StatusBar (); - Add (_statusBar); - }; + _btnAddStatusBar.Clicked += (s, e) => { + if (_statusBar != null) { + return; + } - _btnRemoveStatusBar.Clicked += (s,e) => { - if (_statusBar == null) { - return; - } + _statusBar = new StatusBar (); + Add (_statusBar); + }; - Remove (_statusBar); - _statusBar = null; - DataContext.Items = new List (); - _currentStatusItem = null; - _currentSelectedStatusBar = -1; - SetListViewSource (_currentStatusItem, true); - SetFrameDetails (null); - }; + _btnRemoveStatusBar.Clicked += (s, e) => { + if (_statusBar == null) { + return; + } - SetFrameDetails (); + Remove (_statusBar); + _statusBar = null; + DataContext.Items = new List (); + _currentStatusItem = null; + _currentSelectedStatusBar = -1; + SetListViewSource (_currentStatusItem, true); + SetFrameDetails (null); + }; - var ustringConverter = new UStringValueConverter (); - var listWrapperConverter = new ListWrapperConverter (); + SetFrameDetails (); - var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter); + var ustringConverter = new UStringValueConverter (); + var listWrapperConverter = new ListWrapperConverter (); - void SetFrameDetails (StatusItem statusItem = null) - { - StatusItem newStatusItem; + var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter); - if (statusItem == null) { - newStatusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; - } else { - newStatusItem = statusItem; - } + void SetFrameDetails (StatusItem statusItem = null) + { + StatusItem newStatusItem; - _currentEditStatusItem = newStatusItem; - _frmStatusBarDetails.EditStatusItem (newStatusItem); - var f = _btnOk.Enabled == _frmStatusBarDetails.Enabled; - if (!f) { - _btnOk.Enabled = _frmStatusBarDetails.Enabled; - _btnCancel.Enabled = _frmStatusBarDetails.Enabled; - } + if (statusItem == null) { + newStatusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; + } else { + newStatusItem = statusItem; } - void SetListViewSource (StatusItem _currentStatusItem, bool fill = false) - { - DataContext.Items = new List (); - var statusItem = _currentStatusItem; - if (!fill) { - return; - } - if (statusItem != null) { - foreach (var si in _statusBar.Items) { - DataContext.Items.Add (new DynamicStatusItemList (si.Title, si)); - } - } + _currentEditStatusItem = newStatusItem; + _frmStatusBarDetails.EditStatusItem (newStatusItem); + var f = _btnOk.Enabled == _frmStatusBarDetails.Enabled; + if (!f) { + _btnOk.Enabled = _frmStatusBarDetails.Enabled; + _btnCancel.Enabled = _frmStatusBarDetails.Enabled; } + } - StatusItem CreateNewStatusBar (DynamicStatusItem item) - { - var newStatusItem = new StatusItem (ShortcutHelper.GetShortcutFromTag ( - item.shortcut, StatusBar.ShortcutDelimiter), - item.title, _frmStatusBarDetails.CreateAction (item)); - - return newStatusItem; + void SetListViewSource (StatusItem _currentStatusItem, bool fill = false) + { + DataContext.Items = new List (); + var statusItem = _currentStatusItem; + if (!fill) { + return; } - - void UpdateStatusItem (StatusItem _currentEditStatusItem, DynamicStatusItem statusItem, int index) - { - _currentEditStatusItem = CreateNewStatusBar (statusItem); - _statusBar.Items [index] = _currentEditStatusItem; - if (DataContext.Items.Count == 0) { - DataContext.Items.Add (new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem)); + if (statusItem != null) { + foreach (var si in _statusBar.Items) { + DataContext.Items.Add (new DynamicStatusItemList (si.Title, si)); } - DataContext.Items [index] = new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem); - SetFrameDetails (_currentEditStatusItem); } - - //_frmStatusBarDetails.Initialized += (s, e) => _frmStatusBarDetails.Enabled = false; } - public static string SetTitleText (string title, string shortcut) + StatusItem CreateNewStatusBar (DynamicStatusItem item) { - var txt = title; - var split = title.Split ('~'); - if (split.Length > 1) { - txt = split [2].Trim (); ; - } - if (string.IsNullOrEmpty (shortcut)) { - return txt; - } + var newStatusItem = new StatusItem (ShortcutHelper.GetShortcutFromTag ( + item.shortcut, StatusBar.ShortcutDelimiter), + item.title, _frmStatusBarDetails.CreateAction (item)); - return $"~{shortcut}~ {txt}"; + return newStatusItem; } - } - public class DynamicStatusBarDetails : FrameView { - public StatusItem _statusItem; - public TextField _txtTitle; - public TextView _txtAction; - public TextField _txtShortcut; - - public DynamicStatusBarDetails (StatusItem statusItem = null) : this (statusItem == null ? "Adding New StatusBar Item." : "Editing StatusBar Item.") + void UpdateStatusItem (StatusItem _currentEditStatusItem, DynamicStatusItem statusItem, int index) { - _statusItem = statusItem; + _currentEditStatusItem = CreateNewStatusBar (statusItem); + _statusBar.Items [index] = _currentEditStatusItem; + if (DataContext.Items.Count == 0) { + DataContext.Items.Add (new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem)); + } + DataContext.Items [index] = new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem); + SetFrameDetails (_currentEditStatusItem); } - public DynamicStatusBarDetails (string title) : base (title) - { - var _lblTitle = new Label ("Title:") { - Y = 1 - }; - Add (_lblTitle); - - _txtTitle = new TextField () { - X = Pos.Right (_lblTitle) + 4, - Y = Pos.Top (_lblTitle), - Width = Dim.Fill () - }; - Add (_txtTitle); + //_frmStatusBarDetails.Initialized += (s, e) => _frmStatusBarDetails.Enabled = false; + } - var _lblAction = new Label ("Action:") { - X = Pos.Left (_lblTitle), - Y = Pos.Bottom (_lblTitle) + 1 - }; - Add (_lblAction); + public static string SetTitleText (string title, string shortcut) + { + var txt = title; + var split = title.Split ('~'); + if (split.Length > 1) { + txt = split [2].Trim (); ; + } + if (string.IsNullOrEmpty (shortcut)) { + return txt; + } - _txtAction = new TextView () { - X = Pos.Left (_txtTitle), - Y = Pos.Top (_lblAction), - Width = Dim.Fill (), - Height = 5 - }; - Add (_txtAction); + return $"~{shortcut}~ {txt}"; + } + } - var _lblShortcut = new Label ("Shortcut:") { - X = Pos.Left (_lblTitle), - Y = Pos.Bottom (_txtAction) + 1 - }; - Add (_lblShortcut); + public class DynamicStatusBarDetails : FrameView { + public StatusItem _statusItem; + public TextField _txtTitle; + public TextView _txtAction; + public TextField _txtShortcut; - _txtShortcut = new TextField () { - X = Pos.X (_txtAction), - Y = Pos.Y (_lblShortcut), - Width = Dim.Fill (), - ReadOnly = true - }; - _txtShortcut.KeyDown += (s, e) => { - if (!ProcessKey (e.KeyEvent)) { - return; - } + public DynamicStatusBarDetails (StatusItem statusItem = null) : this (statusItem == null ? "Adding New StatusBar Item." : "Editing StatusBar Item.") + { + _statusItem = statusItem; + } - var k = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (CheckShortcut (k, true)) { - e.Handled = true; - } - }; + public DynamicStatusBarDetails (string title) : base (title) + { + var _lblTitle = new Label ("Title:") { + Y = 1 + }; + Add (_lblTitle); + + _txtTitle = new TextField () { + X = Pos.Right (_lblTitle) + 4, + Y = Pos.Top (_lblTitle), + Width = Dim.Fill () + }; + Add (_txtTitle); + + var _lblAction = new Label ("Action:") { + X = Pos.Left (_lblTitle), + Y = Pos.Bottom (_lblTitle) + 1 + }; + Add (_lblAction); + + _txtAction = new TextView () { + X = Pos.Left (_txtTitle), + Y = Pos.Top (_lblAction), + Width = Dim.Fill (), + Height = 5 + }; + Add (_txtAction); + + var _lblShortcut = new Label ("Shortcut:") { + X = Pos.Left (_lblTitle), + Y = Pos.Bottom (_txtAction) + 1 + }; + Add (_lblShortcut); + + _txtShortcut = new TextField () { + X = Pos.X (_txtAction), + Y = Pos.Y (_lblShortcut), + Width = Dim.Fill (), + ReadOnly = true + }; + _txtShortcut.KeyDown += (s, e) => { + if (!ProcessKey (e)) { + return; + } - bool ProcessKey (KeyEvent ev) - { - switch (ev.Key) { - case Key.CursorUp: - case Key.CursorDown: - case Key.Tab: - case Key.BackTab: - return false; - } + if (CheckShortcut (e.KeyCode, true)) { + e.Handled = true; + } + }; - return true; + bool ProcessKey (Key ev) + { + switch (ev.KeyCode) { + case KeyCode.CursorUp: + case KeyCode.CursorDown: + case KeyCode.Tab: + case KeyCode.Tab | KeyCode.ShiftMask: + return false; } - bool CheckShortcut (Key k, bool pre) - { - var m = _statusItem != null ? _statusItem : new StatusItem (k, "", null); - if (pre && !ShortcutHelper.PreShortcutValidation (k)) { + return true; + } + + bool CheckShortcut (KeyCode k, bool pre) + { + var m = _statusItem != null ? _statusItem : new StatusItem (k, "", null); + if (pre && !ShortcutHelper.PreShortcutValidation (k)) { + _txtShortcut.Text = ""; + return false; + } + if (!pre) { + if (!ShortcutHelper.PostShortcutValidation (ShortcutHelper.GetShortcutFromTag ( + _txtShortcut.Text, StatusBar.ShortcutDelimiter))) { _txtShortcut.Text = ""; return false; } - if (!pre) { - if (!ShortcutHelper.PostShortcutValidation (ShortcutHelper.GetShortcutFromTag ( - _txtShortcut.Text, StatusBar.ShortcutDelimiter))) { - _txtShortcut.Text = ""; - return false; - } - return true; - } - _txtShortcut.Text = ShortcutHelper.GetShortcutTag (k, StatusBar.ShortcutDelimiter); - return true; } + _txtShortcut.Text = Key.ToString (k, StatusBar.ShortcutDelimiter);//ShortcutHelper.GetShortcutTag (k, StatusBar.ShortcutDelimiter); - _txtShortcut.KeyUp += (s, e) => { - var k = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (CheckShortcut (k, false)) { - e.Handled = true; - } - }; - Add (_txtShortcut); - - var _btnShortcut = new Button ("Clear Shortcut") { - X = Pos.X (_lblShortcut), - Y = Pos.Bottom (_txtShortcut) + 1 - }; - _btnShortcut.Clicked += (s,e) => { - _txtShortcut.Text = ""; - }; - Add (_btnShortcut); + return true; } - public DynamicStatusItem EnterStatusItem () - { - var valid = false; - - if (_statusItem == null) { - var m = new DynamicStatusItem (); - _txtTitle.Text = m.title; - _txtAction.Text = m.action; - } else { - EditStatusItem (_statusItem); + _txtShortcut.KeyUp += (s, e) => { + if (CheckShortcut (e.KeyCode, true)) { + e.Handled = true; } + }; + Add (_txtShortcut); + + var _btnShortcut = new Button ("Clear Shortcut") { + X = Pos.X (_lblShortcut), + Y = Pos.Bottom (_txtShortcut) + 1 + }; + _btnShortcut.Clicked += (s, e) => { + _txtShortcut.Text = ""; + }; + Add (_btnShortcut); + } - var _btnOk = new Button ("Ok") { - IsDefault = true, - }; - _btnOk.Clicked += (s,e) => { - if (string.IsNullOrEmpty (_txtTitle.Text)) { - MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); - } else { - if (!string.IsNullOrEmpty (_txtShortcut.Text)) { - _txtTitle.Text = DynamicStatusBarSample.SetTitleText ( - _txtTitle.Text, _txtShortcut.Text); - } - valid = true; - Application.RequestStop (); - } - }; - var _btnCancel = new Button ("Cancel"); - _btnCancel.Clicked += (s,e) => { - _txtTitle.Text = string.Empty; - Application.RequestStop (); - }; - var _dialog = new Dialog (_btnOk, _btnCancel) { Title = "Enter the menu details." }; - - Width = Dim.Fill (); - Height = Dim.Fill () - 1; - _dialog.Add (this); - _txtTitle.SetFocus (); - _txtTitle.CursorPosition = _txtTitle.Text.Length; - Application.Run (_dialog); - - if (valid) { - return new DynamicStatusItem (_txtTitle.Text, _txtAction.Text, _txtShortcut.Text); - } else { - return null; - } + public DynamicStatusItem EnterStatusItem () + { + var valid = false; + + if (_statusItem == null) { + var m = new DynamicStatusItem (); + _txtTitle.Text = m.title; + _txtAction.Text = m.action; + } else { + EditStatusItem (_statusItem); } - public void EditStatusItem (StatusItem statusItem) - { - if (statusItem == null) { - Enabled = false; - CleanEditStatusItem (); - return; + var _btnOk = new Button ("Ok") { + IsDefault = true, + }; + _btnOk.Clicked += (s, e) => { + if (string.IsNullOrEmpty (_txtTitle.Text)) { + MessageBox.ErrorQuery ("Invalid title", "Must enter a valid title!.", "Ok"); } else { - Enabled = true; + if (!string.IsNullOrEmpty (_txtShortcut.Text)) { + _txtTitle.Text = DynamicStatusBarSample.SetTitleText ( + _txtTitle.Text, _txtShortcut.Text); + } + valid = true; + Application.RequestStop (); } - _statusItem = statusItem; - _txtTitle.Text = statusItem?.Title ?? ""; - _txtAction.Text = statusItem != null && statusItem.Action != null ? GetTargetAction (statusItem.Action) : string.Empty; - _txtShortcut.Text = ShortcutHelper.GetShortcutTag (statusItem.Shortcut, StatusBar.ShortcutDelimiter) ?? ""; + }; + var _btnCancel = new Button ("Cancel"); + _btnCancel.Clicked += (s, e) => { + _txtTitle.Text = string.Empty; + Application.RequestStop (); + }; + var _dialog = new Dialog (_btnOk, _btnCancel) { Title = "Enter the menu details." }; + + Width = Dim.Fill (); + Height = Dim.Fill () - 1; + _dialog.Add (this); + _txtTitle.SetFocus (); + _txtTitle.CursorPosition = _txtTitle.Text.Length; + Application.Run (_dialog); + + if (valid) { + return new DynamicStatusItem (_txtTitle.Text, _txtAction.Text, _txtShortcut.Text); + } else { + return null; } + } - void CleanEditStatusItem () - { - _txtTitle.Text = ""; - _txtAction.Text = ""; - _txtShortcut.Text = ""; + public void EditStatusItem (StatusItem statusItem) + { + if (statusItem == null) { + Enabled = false; + CleanEditStatusItem (); + return; + } else { + Enabled = true; } + _statusItem = statusItem; + _txtTitle.Text = statusItem?.Title ?? ""; + _txtAction.Text = statusItem != null && statusItem.Action != null ? GetTargetAction (statusItem.Action) : string.Empty; + _txtShortcut.Text = Key.ToString ((KeyCode)statusItem.Shortcut, StatusBar.ShortcutDelimiter);//ShortcutHelper.GetShortcutTag (statusItem.Shortcut, StatusBar.ShortcutDelimiter) ?? ""; + } - string GetTargetAction (Action action) - { - var me = action.Target; + void CleanEditStatusItem () + { + _txtTitle.Text = ""; + _txtAction.Text = ""; + _txtShortcut.Text = ""; + } - if (me == null) { - throw new ArgumentException (); - } - object v = new object (); - foreach (var field in me.GetType ().GetFields ()) { - if (field.Name == "item") { - v = field.GetValue (me); - } + string GetTargetAction (Action action) + { + var me = action.Target; + + if (me == null) { + throw new ArgumentException (); + } + object v = new object (); + foreach (var field in me.GetType ().GetFields ()) { + if (field.Name == "item") { + v = field.GetValue (me); } - return v == null || !(v is DynamicStatusItem item) ? string.Empty : item.action; } + return v == null || !(v is DynamicStatusItem item) ? string.Empty : item.action; + } - public Action CreateAction (DynamicStatusItem item) - { - return new Action (() => MessageBox.ErrorQuery (item.title, item.action, "Ok")); - } + public Action CreateAction (DynamicStatusItem item) + { + return new Action (() => MessageBox.ErrorQuery (item.title, item.action, "Ok")); } + } - public class DynamicStatusItemModel : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; + public class DynamicStatusItemModel : INotifyPropertyChanged { + public event PropertyChangedEventHandler PropertyChanged; - private string statusBar; - private List items; + private string statusBar; + private List items; - public string StatusBar { - get => statusBar; - set { - if (value != statusBar) { - statusBar = value; - PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ())); - } + public string StatusBar { + get => statusBar; + set { + if (value != statusBar) { + statusBar = value; + PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ())); } } + } - public List Items { - get => items; - set { - if (value != items) { - items = value; - PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ())); - } + public List Items { + get => items; + set { + if (value != items) { + items = value; + PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ())); } } + } - public DynamicStatusItemModel () - { - Items = new List (); - } - - public string GetPropertyName ([CallerMemberName] string propertyName = null) - { - return propertyName; - } + public DynamicStatusItemModel () + { + Items = new List (); } - public interface IValueConverter { - object Convert (object value, object parameter = null); + public string GetPropertyName ([CallerMemberName] string propertyName = null) + { + return propertyName; } + } - public class Binding { - public View Target { get; private set; } - public View Source { get; private set; } + public interface IValueConverter { + object Convert (object value, object parameter = null); + } - public string SourcePropertyName { get; private set; } - public string TargetPropertyName { get; private set; } + public class Binding { + public View Target { get; private set; } + public View Source { get; private set; } - private object sourceDataContext; - private PropertyInfo sourceBindingProperty; - private IValueConverter valueConverter; + public string SourcePropertyName { get; private set; } + public string TargetPropertyName { get; private set; } - public Binding (View source, string sourcePropertyName, View target, string targetPropertyName, IValueConverter valueConverter = null) - { - Target = target; - Source = source; - SourcePropertyName = sourcePropertyName; - TargetPropertyName = targetPropertyName; - sourceDataContext = Source.GetType ().GetProperty ("DataContext").GetValue (Source); - sourceBindingProperty = sourceDataContext.GetType ().GetProperty (SourcePropertyName); - this.valueConverter = valueConverter; - UpdateTarget (); - - var notifier = ((INotifyPropertyChanged)sourceDataContext); - if (notifier != null) { - notifier.PropertyChanged += (s, e) => { - if (e.PropertyName == SourcePropertyName) { - UpdateTarget (); - } - }; - } - } + private object sourceDataContext; + private PropertyInfo sourceBindingProperty; + private IValueConverter valueConverter; - private void UpdateTarget () - { - try { - var sourceValue = sourceBindingProperty.GetValue (sourceDataContext); - if (sourceValue == null) { - return; + public Binding (View source, string sourcePropertyName, View target, string targetPropertyName, IValueConverter valueConverter = null) + { + Target = target; + Source = source; + SourcePropertyName = sourcePropertyName; + TargetPropertyName = targetPropertyName; + sourceDataContext = Source.GetType ().GetProperty ("DataContext").GetValue (Source); + sourceBindingProperty = sourceDataContext.GetType ().GetProperty (SourcePropertyName); + this.valueConverter = valueConverter; + UpdateTarget (); + + var notifier = ((INotifyPropertyChanged)sourceDataContext); + if (notifier != null) { + notifier.PropertyChanged += (s, e) => { + if (e.PropertyName == SourcePropertyName) { + UpdateTarget (); } + }; + } + } - var finalValue = valueConverter?.Convert (sourceValue) ?? sourceValue; - - var targetProperty = Target.GetType ().GetProperty (TargetPropertyName); - targetProperty.SetValue (Target, finalValue); - } catch (Exception ex) { - MessageBox.ErrorQuery ("Binding Error", $"Binding failed: {ex}.", "Ok"); + private void UpdateTarget () + { + try { + var sourceValue = sourceBindingProperty.GetValue (sourceDataContext); + if (sourceValue == null) { + return; } + + var finalValue = valueConverter?.Convert (sourceValue) ?? sourceValue; + + var targetProperty = Target.GetType ().GetProperty (TargetPropertyName); + targetProperty.SetValue (Target, finalValue); + } catch (Exception ex) { + MessageBox.ErrorQuery ("Binding Error", $"Binding failed: {ex}.", "Ok"); } } + } - public class ListWrapperConverter : IValueConverter { - public object Convert (object value, object parameter = null) - { - return new ListWrapper ((IList)value); - } + public class ListWrapperConverter : IValueConverter { + public object Convert (object value, object parameter = null) + { + return new ListWrapper ((IList)value); } + } - public class UStringValueConverter : IValueConverter { - public object Convert (object value, object parameter = null) - { - var data = Encoding.ASCII.GetBytes (value.ToString ()); - return StringExtensions.ToString (data); - } + public class UStringValueConverter : IValueConverter { + public object Convert (object value, object parameter = null) + { + var data = Encoding.ASCII.GetBytes (value.ToString ()); + return StringExtensions.ToString (data); } } } diff --git a/UICatalog/Scenarios/Editor.cs b/UICatalog/Scenarios/Editor.cs index 45c6f704fd..f2667caab7 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -75,19 +75,19 @@ public override void Init () new MenuItem ("_Quit", "", () => Quit()), }), new MenuBarItem ("_Edit", new MenuItem [] { - new MenuItem ("_Copy", "", () => Copy(),null,null, Key.CtrlMask | Key.C), - new MenuItem ("C_ut", "", () => Cut(),null,null, Key.CtrlMask | Key.W), - new MenuItem ("_Paste", "", () => Paste(),null,null, Key.CtrlMask | Key.Y), + new MenuItem ("_Copy", "", () => Copy(),null,null, KeyCode.CtrlMask | KeyCode.C), + new MenuItem ("C_ut", "", () => Cut(),null,null, KeyCode.CtrlMask | KeyCode.W), + new MenuItem ("_Paste", "", () => Paste(),null,null, KeyCode.CtrlMask | KeyCode.Y), null, - new MenuItem ("_Find", "", () => Find(),null,null, Key.CtrlMask | Key.S), - new MenuItem ("Find _Next", "", () => FindNext(),null,null, Key.CtrlMask | Key.ShiftMask | Key.S), - new MenuItem ("Find P_revious", "", () => FindPrevious(),null,null, Key.CtrlMask | Key.ShiftMask | Key.AltMask | Key.S), - new MenuItem ("_Replace", "", () => Replace(),null,null, Key.CtrlMask | Key.R), - new MenuItem ("Replace Ne_xt", "", () => ReplaceNext(),null,null, Key.CtrlMask | Key.ShiftMask | Key.R), - new MenuItem ("Replace Pre_vious", "", () => ReplacePrevious(),null,null, Key.CtrlMask | Key.ShiftMask | Key.AltMask | Key.R), - new MenuItem ("Replace _All", "", () => ReplaceAll(),null,null, Key.CtrlMask | Key.ShiftMask | Key.AltMask | Key.A), + new MenuItem ("_Find", "", () => Find(),null,null, KeyCode.CtrlMask | KeyCode.S), + new MenuItem ("Find _Next", "", () => FindNext(),null,null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.S), + new MenuItem ("Find P_revious", "", () => FindPrevious(),null,null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.S), + new MenuItem ("_Replace", "", () => Replace(),null,null, KeyCode.CtrlMask | KeyCode.R), + new MenuItem ("Replace Ne_xt", "", () => ReplaceNext(),null,null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.R), + new MenuItem ("Replace Pre_vious", "", () => ReplacePrevious(),null,null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.R), + new MenuItem ("Replace _All", "", () => ReplaceAll(),null,null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.A), null, - new MenuItem ("_Select All", "", () => SelectAll(),null,null, Key.CtrlMask | Key.T) + new MenuItem ("_Select All", "", () => SelectAll(),null,null, KeyCode.CtrlMask | KeyCode.T) }), new MenuBarItem ("_ScrollBarView", CreateKeepChecked ()), new MenuBarItem ("_Cursor", CreateCursorRadio ()), @@ -113,15 +113,15 @@ public override void Init () Application.Top.Add (menu); - var siCursorPosition = new StatusItem (Key.Null, "", null); + var siCursorPosition = new StatusItem (KeyCode.Null, "", null); var statusBar = new StatusBar (new StatusItem [] { siCursorPosition, - new StatusItem(Key.F2, "~F2~ Open", () => Open()), - new StatusItem(Key.F3, "~F3~ Save", () => Save()), - new StatusItem(Key.F4, "~F4~ Save As", () => SaveAs()), + new StatusItem(KeyCode.F2, "~F2~ Open", () => Open()), + new StatusItem(KeyCode.F3, "~F3~ Save", () => Save()), + new StatusItem(KeyCode.F4, "~F4~ Save As", () => SaveAs()), new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), - new StatusItem(Key.Null, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null) + new StatusItem(KeyCode.Null, $"OS Clipboard IsSupported : {Clipboard.IsSupported}", null) }); _textView.UnwrappedCursorPosition += (s, e) => { @@ -176,22 +176,20 @@ public override void Init () _scrollBar.Refresh (); }; - Win.KeyPressed += (s, e) => { - var keys = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (_winDialog != null && (e.KeyEvent.Key == Key.Esc - || e.KeyEvent.Key == Application.QuitKey)) { + Win.KeyDown += (s, e) => { + if (_winDialog != null && (e.KeyCode == KeyCode.Esc || e == Application.QuitKey)) { DisposeWinDialog (); - } else if (e.KeyEvent.Key == Application.QuitKey) { + } else if (e == Application.QuitKey) { Quit (); e.Handled = true; - } else if (_winDialog != null && keys == (Key.Tab | Key.CtrlMask)) { + } else if (_winDialog != null && e.KeyCode == (KeyCode.Tab | KeyCode.CtrlMask)) { if (_tabView.SelectedTab == _tabView.Tabs.ElementAt (_tabView.Tabs.Count - 1)) { _tabView.SelectedTab = _tabView.Tabs.ElementAt (0); } else { _tabView.SwitchTabBy (1); } e.Handled = true; - } else if (_winDialog != null && keys == (Key.Tab | Key.CtrlMask | Key.ShiftMask)) { + } else if (_winDialog != null && e.KeyCode == (KeyCode.Tab | KeyCode.CtrlMask | KeyCode.ShiftMask)) { if (_tabView.SelectedTab == _tabView.Tabs.ElementAt (0)) { _tabView.SelectedTab = _tabView.Tabs.ElementAt (_tabView.Tabs.Count - 1); } else { @@ -233,7 +231,7 @@ private void LoadFile () // FIXED: BUGBUG: #452 TextView.LoadFile keeps file open and provides no way of closing it _textView.Load (_fileName); //_textView.Text = System.IO.File.ReadAllText (_fileName); - _originalText = Encoding.Unicode.GetBytes(_textView.Text); + _originalText = Encoding.Unicode.GetBytes (_textView.Text); Win.Title = _fileName; _saved = true; } @@ -429,7 +427,7 @@ private bool SaveFile (string title, string file) Win.Title = title; _fileName = file; System.IO.File.WriteAllText (_fileName, _textView.Text); - _originalText = Encoding.Unicode.GetBytes(_textView.Text); + _originalText = Encoding.Unicode.GetBytes (_textView.Text); _saved = true; _textView.ClearHistoryChanges (); MessageBox.Query ("Save File", "File was successfully saved.", "Ok"); @@ -770,7 +768,7 @@ private void CreateFindReplace (bool isFind = true) private void SetFindText () { - _textToFind = !string.IsNullOrEmpty(_textView.SelectedText) + _textToFind = !string.IsNullOrEmpty (_textView.SelectedText) ? _textView.SelectedText : string.IsNullOrEmpty (_textToFind) ? "" : _textToFind; @@ -809,7 +807,7 @@ private View FindTab () X = Pos.Right (txtToFind) + 1, Y = Pos.Top (label), Width = 20, - Enabled = !string.IsNullOrEmpty(txtToFind.Text), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), TextAlignment = TextAlignment.Centered, IsDefault = true, AutoSize = false @@ -821,7 +819,7 @@ private View FindTab () X = Pos.Right (txtToFind) + 1, Y = Pos.Top (btnFindNext) + 1, Width = 20, - Enabled = !string.IsNullOrEmpty(txtToFind.Text), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), TextAlignment = TextAlignment.Centered, AutoSize = false }; @@ -831,8 +829,8 @@ private View FindTab () txtToFind.TextChanged += (s, e) => { _textToFind = txtToFind.Text; _textView.FindTextChanged (); - btnFindNext.Enabled = !string.IsNullOrEmpty(txtToFind.Text); - btnFindPrevious.Enabled = !string.IsNullOrEmpty(txtToFind.Text); + btnFindNext.Enabled = !string.IsNullOrEmpty (txtToFind.Text); + btnFindPrevious.Enabled = !string.IsNullOrEmpty (txtToFind.Text); }; var btnCancel = new Button ("Cancel") { @@ -901,7 +899,7 @@ private View ReplaceTab () X = Pos.Right (txtToFind) + 1, Y = Pos.Top (label), Width = 20, - Enabled = !string.IsNullOrEmpty(txtToFind.Text), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), TextAlignment = TextAlignment.Centered, IsDefault = true, AutoSize = false @@ -930,7 +928,7 @@ private View ReplaceTab () X = Pos.Right (txtToFind) + 1, Y = Pos.Top (btnFindNext) + 1, Width = 20, - Enabled = !string.IsNullOrEmpty(txtToFind.Text), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), TextAlignment = TextAlignment.Centered, AutoSize = false }; @@ -941,7 +939,7 @@ private View ReplaceTab () X = Pos.Right (txtToFind) + 1, Y = Pos.Top (btnFindPrevious) + 1, Width = 20, - Enabled = !string.IsNullOrEmpty(txtToFind.Text), + Enabled = !string.IsNullOrEmpty (txtToFind.Text), TextAlignment = TextAlignment.Centered, AutoSize = false }; @@ -951,9 +949,9 @@ private View ReplaceTab () txtToFind.TextChanged += (s, e) => { _textToFind = txtToFind.Text; _textView.FindTextChanged (); - btnFindNext.Enabled = !string.IsNullOrEmpty(txtToFind.Text); - btnFindPrevious.Enabled = !string.IsNullOrEmpty(txtToFind.Text); - btnReplaceAll.Enabled = !string.IsNullOrEmpty(txtToFind.Text); + btnFindNext.Enabled = !string.IsNullOrEmpty (txtToFind.Text); + btnFindPrevious.Enabled = !string.IsNullOrEmpty (txtToFind.Text); + btnReplaceAll.Enabled = !string.IsNullOrEmpty (txtToFind.Text); }; var btnCancel = new Button ("Cancel") { diff --git a/UICatalog/Scenarios/Generic.cs b/UICatalog/Scenarios/Generic.cs index 9d4ce4e32b..3a162fa85b 100644 --- a/UICatalog/Scenarios/Generic.cs +++ b/UICatalog/Scenarios/Generic.cs @@ -1,42 +1,41 @@ using Terminal.Gui; -namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Generic", Description: "Generic sample - A template for creating new Scenarios")] - [ScenarioCategory ("Controls")] - public class MyScenario : Scenario { - public override void Init () - { - // The base `Scenario.Init` implementation: - // - Calls `Application.Init ()` - // - Adds a full-screen Window to Application.Top with a title - // that reads "Press to Quit". Access this Window with `this.Win`. - // - Sets the Theme & the ColorScheme property of `this.Win` to `colorScheme`. - // To override this, implement an override of `Init`. +namespace UICatalog.Scenarios; +[ScenarioMetadata (Name: "Generic", Description: "Generic sample - A template for creating new Scenarios")] +[ScenarioCategory ("Controls")] +public class MyScenario : Scenario { + public override void Init () + { + // The base `Scenario.Init` implementation: + // - Calls `Application.Init ()` + // - Adds a full-screen Window to Application.Top with a title + // that reads "Press to Quit". Access this Window with `this.Win`. + // - Sets the Theme & the ColorScheme property of `this.Win` to `colorScheme`. + // To override this, implement an override of `Init`. - //base.Init (); + //base.Init (); - // A common, alternate, implementation where `this.Win` is not used is below. This code - // leverages ConfigurationManager to borrow the color scheme settings from UICatalog: + // A common, alternate, implementation where `this.Win` is not used is below. This code + // leverages ConfigurationManager to borrow the color scheme settings from UICatalog: - Application.Init (); - ConfigurationManager.Themes.Theme = Theme; - ConfigurationManager.Apply (); - Application.Top.ColorScheme = Colors.ColorSchemes [TopLevelColorScheme]; - } + Application.Init (); + ConfigurationManager.Themes.Theme = Theme; + ConfigurationManager.Apply (); + Application.Top.ColorScheme = Colors.ColorSchemes [TopLevelColorScheme]; + } - public override void Setup () - { - // Put scenario code here (in a real app, this would be the code - // that would setup the app before `Application.Run` is called`). - // With a Scenario, after UI Catalog calls `Scenario.Setup` it calls - // `Scenario.Run` which calls `Application.Run`. Example: + public override void Setup () + { + // Put scenario code here (in a real app, this would be the code + // that would setup the app before `Application.Run` is called`). + // With a Scenario, after UI Catalog calls `Scenario.Setup` it calls + // `Scenario.Run` which calls `Application.Run`. Example: - var button = new Button ("Press me!") { - AutoSize = false, - X = Pos.Center (), - Y = Pos.Center (), - }; - Application.Top.Add (button); - } + var button = new Button ("Press me!") { + AutoSize = false, + X = Pos.Center (), + Y = Pos.Center (), + }; + Application.Top.Add (button); } -} \ No newline at end of file +} diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 9f79be4c29..215fee1709 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -85,7 +85,7 @@ public override void Setup () var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), - new StatusItem(Key.CtrlMask | Key.G, "~^G~ Next", ()=>graphs[currentGraph++%graphs.Length]()), + new StatusItem(KeyCode.CtrlMask | KeyCode.G, "~^G~ Next", ()=>graphs[currentGraph++%graphs.Length]()), }); Application.Top.Add (statusBar); } diff --git a/UICatalog/Scenarios/HexEditor.cs b/UICatalog/Scenarios/HexEditor.cs index c81853ca5b..ead26d81b5 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -55,10 +55,10 @@ public override void Setup () Application.Top.Add (menu); _statusBar = new StatusBar (new StatusItem [] { - new StatusItem(Key.F2, "~F2~ Open", () => Open()), - new StatusItem(Key.F3, "~F3~ Save", () => Save()), + new StatusItem(KeyCode.F2, "~F2~ Open", () => Open()), + new StatusItem(KeyCode.F3, "~F3~ Save", () => Save()), new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), - _siPositionChanged = new StatusItem(Key.Null, + _siPositionChanged = new StatusItem(KeyCode.Null, $"Position: {_hexView.Position} Line: {_hexView.CursorPosition.Y} Col: {_hexView.CursorPosition.X} Line length: {_hexView.BytesPerLine}", () => {}) }); Application.Top.Add (_statusBar); diff --git a/UICatalog/Scenarios/InteractiveTree.cs b/UICatalog/Scenarios/InteractiveTree.cs index 26b26d1fde..5bf061a921 100644 --- a/UICatalog/Scenarios/InteractiveTree.cs +++ b/UICatalog/Scenarios/InteractiveTree.cs @@ -30,23 +30,23 @@ public override void Setup () Width = Dim.Fill (), Height = Dim.Fill (1), }; - treeView.KeyPressed += TreeView_KeyPress; + treeView.KeyDown += TreeView_KeyPress; Win.Add (treeView); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), - new StatusItem(Key.CtrlMask | Key.C, "~^C~ Add Child", () => AddChildNode()), - new StatusItem(Key.CtrlMask | Key.T, "~^T~ Add Root", () => AddRootNode()), - new StatusItem(Key.CtrlMask | Key.R, "~^R~ Rename Node", () => RenameNode()), + new StatusItem(KeyCode.CtrlMask | KeyCode.C, "~^C~ Add Child", () => AddChildNode()), + new StatusItem(KeyCode.CtrlMask | KeyCode.T, "~^T~ Add Root", () => AddRootNode()), + new StatusItem(KeyCode.CtrlMask | KeyCode.R, "~^R~ Rename Node", () => RenameNode()), }); Application.Top.Add (statusBar); } - private void TreeView_KeyPress (object sender, KeyEventEventArgs obj) + private void TreeView_KeyPress (object sender, Key obj) { - if (obj.KeyEvent.Key == Key.DeleteChar) { + if (obj.KeyCode == KeyCode.DeleteChar) { var toDelete = treeView.SelectedObject; diff --git a/UICatalog/Scenarios/Keys.cs b/UICatalog/Scenarios/Keys.cs index 6594daa07c..44efc18268 100644 --- a/UICatalog/Scenarios/Keys.cs +++ b/UICatalog/Scenarios/Keys.cs @@ -1,178 +1,136 @@ -using System.Text; -using System.Collections.Generic; +using System.Collections.Generic; using Terminal.Gui; -namespace UICatalog.Scenarios { - [ScenarioMetadata (Name: "Keys", Description: "Shows keyboard input handling.")] - [ScenarioCategory ("Mouse and Keyboard")] - public class Keys : Scenario { +namespace UICatalog.Scenarios; - class TestWindow : Window { - public List _processKeyList = new List (); - public List _processHotKeyList = new List (); - public List _processColdKeyList = new List (); +[ScenarioMetadata (Name: "Keys", Description: "Shows keyboard input handling.")] +[ScenarioCategory ("Mouse and Keyboard")] +public class Keys : Scenario { - public override bool ProcessKey (KeyEvent keyEvent) - { - _processKeyList.Add (keyEvent.ToString ()); - return base.ProcessKey (keyEvent); - } + public override void Setup () + { + List keyPressedList = new List (); + List invokingKeyBindingsList = new List (); - public override bool ProcessHotKey (KeyEvent keyEvent) - { - _processHotKeyList.Add (keyEvent.ToString ()); - return base.ProcessHotKey (keyEvent); - } + var editLabel = new Label ("Type text here:") { + X = 0, + Y = 0, + }; + Win.Add (editLabel); - public override bool ProcessColdKey (KeyEvent keyEvent) - { - _processColdKeyList.Add (keyEvent.ToString ()); + var edit = new TextField ("") { + X = Pos.Right (editLabel) + 1, + Y = Pos.Top (editLabel), + Width = Dim.Fill (2), + }; + Win.Add (edit); - return base.ProcessColdKey (keyEvent); - } - } + edit.KeyDown += (s, a) => { + keyPressedList.Add (a.ToString ()); + }; - public override void Init () - { - Application.Init (); - ConfigurationManager.Themes.Theme = Theme; - ConfigurationManager.Apply (); - - Win = new TestWindow () { - Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}", - X = 0, - Y = 0, - Width = Dim.Fill (), - Height = Dim.Fill (), - ColorScheme = Colors.ColorSchemes [TopLevelColorScheme], - }; - Application.Top.Add (Win); - } - public override void Setup () - { - // Type text here: ______ - var editLabel = new Label ("Type text here:") { - X = 0, - Y = 0, - }; - Win.Add (editLabel); - var edit = new TextField ("") { - X = Pos.Right (editLabel) + 1, - Y = Pos.Top (editLabel), - Width = Dim.Fill (2), - }; - Win.Add (edit); - - // Last KeyPress: ______ - var keyPressedLabel = new Label ("Last Application.KeyPress:") { - X = Pos.Left (editLabel), - Y = Pos.Top (editLabel) + 2, - }; - Win.Add (keyPressedLabel); - var labelKeypress = new Label ("") { - X = Pos.Left (edit), - Y = Pos.Top (keyPressedLabel), - TextAlignment = Terminal.Gui.TextAlignment.Centered, - ColorScheme = Colors.Error, - AutoSize = true - }; - Win.Add (labelKeypress); - - Win.KeyPressed += (s, e) => labelKeypress.Text = e.KeyEvent.ToString (); - - // Key stroke log: - var keyLogLabel = new Label ("Key event log:") { - X = Pos.Left (editLabel), - Y = Pos.Top (editLabel) + 4, - }; - Win.Add (keyLogLabel); - var fakeKeyPress = new KeyEvent (Key.CtrlMask | Key.A, new KeyModifiers () { - Alt = true, - Ctrl = true, - Shift = true - }); - var maxLogEntry = $"Key{"",-5}: {fakeKeyPress}".Length; - var yOffset = (Application.Top == Application.Top ? 1 : 6); - var keyEventlist = new List (); - var keyEventListView = new ListView (keyEventlist) { - X = 0, - Y = Pos.Top (keyLogLabel) + yOffset, - Width = Dim.Percent (30), - Height = Dim.Fill (), - }; - keyEventListView.ColorScheme = Colors.TopLevel; - Win.Add (keyEventListView); - - // ProcessKey log: - var processKeyLogLabel = new Label ("ProcessKey log:") { - X = Pos.Right (keyEventListView) + 1, - Y = Pos.Top (editLabel) + 4, - }; - Win.Add (processKeyLogLabel); - - maxLogEntry = $"{fakeKeyPress}".Length; - yOffset = (Application.Top == Application.Top ? 1 : 6); - var processKeyListView = new ListView (((TestWindow)Win)._processKeyList) { - X = Pos.Left (processKeyLogLabel), - Y = Pos.Top (processKeyLogLabel) + yOffset, - Width = Dim.Percent (30), - Height = Dim.Fill (), - }; - processKeyListView.ColorScheme = Colors.TopLevel; - Win.Add (processKeyListView); - - // ProcessHotKey log: - // BUGBUG: Label is not positioning right with Pos, so using TextField instead - var processHotKeyLogLabel = new Label ("ProcessHotKey log:") { - X = Pos.Right (processKeyListView) + 1, - Y = Pos.Top (editLabel) + 4, - }; - Win.Add (processHotKeyLogLabel); - - yOffset = (Application.Top == Application.Top ? 1 : 6); - var processHotKeyListView = new ListView (((TestWindow)Win)._processHotKeyList) { - X = Pos.Left (processHotKeyLogLabel), - Y = Pos.Top (processHotKeyLogLabel) + yOffset, - Width = Dim.Percent (20), - Height = Dim.Fill (), - }; - processHotKeyListView.ColorScheme = Colors.TopLevel; - Win.Add (processHotKeyListView); - - // ProcessColdKey log: - // BUGBUG: Label is not positioning right with Pos, so using TextField instead - var processColdKeyLogLabel = new Label ("ProcessColdKey log:") { - X = Pos.Right (processHotKeyListView) + 1, - Y = Pos.Top (editLabel) + 4, - }; - Win.Add (processColdKeyLogLabel); - - yOffset = (Application.Top == Application.Top ? 1 : 6); - var processColdKeyListView = new ListView (((TestWindow)Win)._processColdKeyList) { - X = Pos.Left (processColdKeyLogLabel), - Y = Pos.Top (processColdKeyLogLabel) + yOffset, - Width = Dim.Percent (20), - Height = Dim.Fill (), - }; - - Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down"); - Application.KeyPressed += (s, a) => KeyDownPressUp (a, "Press"); - Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up"); - - void KeyDownPressUp (KeyEventEventArgs args, string updown) - { - // BUGBUG: KeyEvent.ToString is badly broken - var msg = $"Key{updown,-5}: {args.KeyEvent}"; - keyEventlist.Add (msg); - keyEventListView.MoveDown (); - processKeyListView.MoveDown (); - processColdKeyListView.MoveDown (); - processHotKeyListView.MoveDown (); + edit.InvokingKeyBindings += (s, a) => { + if (edit.KeyBindings.TryGet (a, out var binding)) { + invokingKeyBindingsList.Add ($"{a}: {string.Join (",", binding.Commands)}"); } - - processColdKeyListView.ColorScheme = Colors.TopLevel; - Win.Add (processColdKeyListView); + }; + + // Last KeyPress: ______ + var keyPressedLabel = new Label ("Last TextView.KeyPressed:") { + X = Pos.Left (editLabel), + Y = Pos.Top (editLabel) + 1, + }; + Win.Add (keyPressedLabel); + var labelTextViewKeypress = new Label ("") { + X = Pos.Right (keyPressedLabel) + 1, + Y = Pos.Top (keyPressedLabel), + TextAlignment = Terminal.Gui.TextAlignment.Centered, + ColorScheme = Colors.Error, + AutoSize = true + }; + Win.Add (labelTextViewKeypress); + + edit.KeyDown += (s, e) => labelTextViewKeypress.Text = e.ToString (); + + keyPressedLabel = new Label ("Last Application.KeyDown:") { + X = Pos.Left (keyPressedLabel), + Y = Pos.Bottom (keyPressedLabel), + }; + Win.Add (keyPressedLabel); + var labelAppKeypress = new Label ("") { + X = Pos.Right (keyPressedLabel) + 1, + Y = Pos.Top (keyPressedLabel), + TextAlignment = Terminal.Gui.TextAlignment.Centered, + ColorScheme = Colors.Error, + AutoSize = true + }; + Win.Add (labelAppKeypress); + + Application.KeyDown += (s, e) => labelAppKeypress.Text = e.ToString (); + + // Key stroke log: + var keyLogLabel = new Label ("Application Key Events:") { + X = Pos.Left (editLabel), + Y = Pos.Top (editLabel) + 4, + }; + Win.Add (keyLogLabel); + var maxKeyString = Key.CursorRight.WithAlt.WithCtrl.WithShift.ToString ().Length; + var yOffset = 1; + var keyEventlist = new List (); + var keyEventListView = new ListView (keyEventlist) { + X = 0, + Y = Pos.Top (keyLogLabel) + yOffset, + Width = "Key Down:".Length + maxKeyString, + Height = Dim.Fill (), + }; + keyEventListView.ColorScheme = Colors.TopLevel; + Win.Add (keyEventListView); + + // OnKeyPressed + var onKeyPressedLabel = new Label ("TextView KeyDown:") { + X = Pos.Right (keyEventListView) + 1, + Y = Pos.Top (editLabel) + 4, + }; + Win.Add (onKeyPressedLabel); + + yOffset = 1; + var onKeyPressedListView = new ListView (keyPressedList) { + X = Pos.Left (onKeyPressedLabel), + Y = Pos.Top (onKeyPressedLabel) + yOffset, + Width = maxKeyString, + Height = Dim.Fill (), + }; + onKeyPressedListView.ColorScheme = Colors.TopLevel; + Win.Add (onKeyPressedListView); + + // OnInvokeKeyBindings + var onInvokingKeyBindingsLabel = new Label ("TextView InvokingKeyBindings:") { + X = Pos.Right (onKeyPressedListView) + 1, + Y = Pos.Top (editLabel) + 4, + }; + Win.Add (onInvokingKeyBindingsLabel); + var onInvokingKeyBindingsListView = new ListView (invokingKeyBindingsList) { + X = Pos.Left (onInvokingKeyBindingsLabel), + Y = Pos.Top (onInvokingKeyBindingsLabel) + yOffset, + Width = Dim.Fill (1), + Height = Dim.Fill (), + }; + onInvokingKeyBindingsListView.ColorScheme = Colors.TopLevel; + Win.Add (onInvokingKeyBindingsListView); + + //Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down"); + Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down"); + Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up"); + + void KeyDownPressUp (Key args, string updown) + { + // BUGBUG: KeyEvent.ToString is badly broken + var msg = $"Key{updown,-7}: {args}"; + keyEventlist.Add (msg); + keyEventListView.MoveDown (); + onKeyPressedListView.MoveDown (); + onInvokingKeyBindingsListView.MoveDown (); } } } \ No newline at end of file diff --git a/UICatalog/Scenarios/LineDrawing.cs b/UICatalog/Scenarios/LineDrawing.cs index 6dd0fa1d87..a4fb8e0a4a 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -33,7 +33,7 @@ public override void Setup () Win.Add (canvas); Win.Add (tools); - Win.KeyPressed += (s,e) => { e.Handled = canvas.ProcessKey (e.KeyEvent); }; + Win.KeyDown += (s,e) => { e.Handled = canvas.OnKeyDown (e); }; } class ToolsView : Window { @@ -108,9 +108,11 @@ public DrawingArea () Stack undoHistory = new (); - public override bool ProcessKey (KeyEvent e) + //// BUGBUG: Why is this not handled by a key binding??? + public override bool OnKeyDown (Key e) { - if (e.Key == (Key.Z | Key.CtrlMask)) { + // BUGBUG: These should be implemented with key bindings + if (e.KeyCode == (KeyCode.Z | KeyCode.CtrlMask)) { var pop = _currentLayer.RemoveLastLine (); if(pop != null) { undoHistory.Push (pop); @@ -119,7 +121,7 @@ public override bool ProcessKey (KeyEvent e) } } - if (e.Key == (Key.Y | Key.CtrlMask)) { + if (e.KeyCode == (KeyCode.Y | KeyCode.CtrlMask)) { if (undoHistory.Any()) { var pop = undoHistory.Pop (); _currentLayer.AddLine(pop); @@ -127,9 +129,9 @@ public override bool ProcessKey (KeyEvent e) return true; } } - - return base.ProcessKey (e); + return false; } + internal void AddLayer () { _currentLayer = new LineCanvas (); diff --git a/UICatalog/Scenarios/ListColumns.cs b/UICatalog/Scenarios/ListColumns.cs index c3c38f0695..b71222cff7 100644 --- a/UICatalog/Scenarios/ListColumns.cs +++ b/UICatalog/Scenarios/ListColumns.cs @@ -80,9 +80,9 @@ public override void Setup () Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { - new StatusItem(Key.F2, "~F2~ OpenBigListEx", () => OpenSimpleList (true)), - new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample ()), - new StatusItem(Key.F4, "~F4~ OpenSmListEx", () => OpenSimpleList (false)), + new StatusItem(KeyCode.F2, "~F2~ OpenBigListEx", () => OpenSimpleList (true)), + new StatusItem(KeyCode.F3, "~F3~ CloseExample", () => CloseExample ()), + new StatusItem(KeyCode.F4, "~F4~ OpenSmListEx", () => OpenSimpleList (false)), new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), }); Application.Top.Add (statusBar); @@ -101,7 +101,7 @@ public override void Setup () Win.Add (selectedCellLabel); listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{listColView.SelectedRow},{listColView.SelectedColumn}"; }; - listColView.KeyPressed += TableViewKeyPress; + listColView.KeyDown += TableViewKeyPress; SetupScrollBar (); @@ -119,7 +119,7 @@ public override void Setup () listColView.ScreenToCell (e.MouseEvent.X, e.MouseEvent.Y, out int? clickedCol); }; - listColView.AddKeyBinding (Key.Space, Command.ToggleChecked); + listColView.KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); } private void SetupScrollBar () @@ -153,9 +153,9 @@ private void SetupScrollBar () } - private void TableViewKeyPress (object sender, KeyEventEventArgs e) + private void TableViewKeyPress (object sender, Key e) { - if (e.KeyEvent.Key == Key.DeleteChar) { + if (e.KeyCode == KeyCode.DeleteChar) { // set all selected cells to null foreach (var pt in listColView.GetAllSelectedCells ()) { diff --git a/UICatalog/Scenarios/MenuBarScenario.cs b/UICatalog/Scenarios/MenuBarScenario.cs new file mode 100644 index 0000000000..4677c12544 --- /dev/null +++ b/UICatalog/Scenarios/MenuBarScenario.cs @@ -0,0 +1,217 @@ +using System; +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("MenuBar", "Demonstrates the MenuBar using the same menu used in unit tests.")] +[ScenarioCategory ("Controls")] [ScenarioCategory ("Menu")] +public class MenuBarScenario : Scenario { + /// + /// This method creates at test menu bar. It is called by the MenuBar unit tests so + /// it's possible to do both unit testing and user-experience testing with the same setup. + /// + /// + /// + public static MenuBar CreateTestMenu (Func actionFn) + { + var mb = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + new MenuItem ("_New", "", () => actionFn ("New"), null, null, KeyCode.CtrlMask | KeyCode.N), + new MenuItem ("_Open", "", () => actionFn ("Open"), null, null, KeyCode.CtrlMask | KeyCode.O), + new MenuItem ("_Save", "", () => actionFn ("Save"), null, null, KeyCode.CtrlMask | KeyCode.S), + null, + // Don't use Ctrl-Q so we can disambiguate between quitting and closing the toplevel + new MenuItem ("_Quit", "", () => actionFn ("Quit"), null, null, KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Q) + }), + new MenuBarItem ("_Edit", new MenuItem [] { + new MenuItem ("_Copy", "", () => actionFn ("Copy"), null, null, KeyCode.CtrlMask | KeyCode.C), + new MenuItem ("C_ut", "", () => actionFn ("Cut"), null, null, KeyCode.CtrlMask | KeyCode.X), + new MenuItem ("_Paste", "", () => actionFn ("Paste"), null, null, KeyCode.CtrlMask | KeyCode.V), + new MenuBarItem ("_Find and Replace", new MenuItem [] { + new MenuItem ("F_ind", "", () => actionFn ("Find"), null, null, KeyCode.CtrlMask | KeyCode.F), + new MenuItem ("_Replace", "", () => actionFn ("Replace"), null, null, KeyCode.CtrlMask | KeyCode.H), + new MenuBarItem ("_3rd Level", new MenuItem [] { + new MenuItem ("_1st", "", () => actionFn ("1"), null, null, KeyCode.F1), + new MenuItem ("_2nd", "", () => actionFn ("2"), null, null, KeyCode.F2), + }), + new MenuBarItem ("_4th Level", new MenuItem [] { + new MenuItem ("_5th", "", () => actionFn ("5"), null, null, KeyCode.CtrlMask | KeyCode.D5), + new MenuItem ("_6th", "", () => actionFn ("6"), null, null, KeyCode.CtrlMask | KeyCode.D6), + }), + }), + new MenuItem ("_Select All", "", () => actionFn ("Select All"), null, null, KeyCode.CtrlMask | KeyCode.ShiftMask | KeyCode.S), + }), + new MenuBarItem ("_About", "Top-Level", () => actionFn ("About"), null, null), + }); + mb.UseKeysUpDownAsKeysLeftRight = true; + mb.Key = KeyCode.F9; + mb.Title = "TestMenuBar"; + return mb; + } + + // Don't create a Window, just return the top-level view + public override void Init () + { + Application.Init (); + Application.Top.ColorScheme = Colors.Base; + } + + Label _currentMenuBarItem; + Label _currentMenuItem; + Label _lastAction; + Label _focusedView; + Label _lastKey; + + public override void Setup () + { + MenuItem mbiCurrent = null; + MenuItem miCurrent = null; + + var label = new Label () { + X = 0, + Y = 10, + Text = "Last Key: " + }; + Application.Top.Add (label); + + _lastKey = new Label () { + X = Pos.Right (label), + Y = Pos.Top (label), + Text = "" + }; + + Application.Top.Add (_lastKey); + label = new Label () { + X = 0, + Y = Pos.Bottom (label), + Text = "Current MenuBarItem: " + }; + Application.Top.Add (label); + + _currentMenuBarItem = new Label () { + X = Pos.Right(label), + Y = Pos.Top (label), + Text = "" + }; + Application.Top.Add (_currentMenuBarItem); + + label = new Label () { + X = 0, + Y = Pos.Bottom(label), + Text = "Current MenuItem: " + }; + Application.Top.Add (label); + + _currentMenuItem = new Label () { + X = Pos.Right (label), + Y = Pos.Top (label), + Text = "" + }; + Application.Top.Add (_currentMenuItem); + + label = new Label () { + X = 0, + Y = Pos.Bottom (label), + Text = "Last Action: " + }; + Application.Top.Add (label); + + _lastAction = new Label () { + X = Pos.Right (label), + Y = Pos.Top (label), + Text = "" + }; + Application.Top.Add (_lastAction); + + label = new Label () { + X = 0, + Y = Pos.Bottom (label), + Text = "Focused View: " + }; + Application.Top.Add (label); + + _focusedView = new Label () { + X = Pos.Right (label), + Y = Pos.Top (label), + Text = "" + }; + Application.Top.Add (_focusedView); + + var menuBar = CreateTestMenu ((s) => { + _lastAction.Text = s; + return true; + }); + + menuBar.MenuOpening += (s, e) => { + mbiCurrent = e.CurrentMenu; + SetCurrentMenuBarItem (mbiCurrent); + SetCurrentMenuItem (miCurrent); + _lastAction.Text = string.Empty; + }; + menuBar.MenuOpened += (s, e) => { + miCurrent = e.MenuItem; + SetCurrentMenuBarItem (mbiCurrent); + SetCurrentMenuItem (miCurrent); + }; + menuBar.MenuClosing += (s, e) => { + mbiCurrent = null; + miCurrent = null; + SetCurrentMenuBarItem (mbiCurrent); + SetCurrentMenuItem (miCurrent); + }; + + Application.KeyDown += (s, e) => { + _lastAction.Text = string.Empty; + _lastKey.Text = e.ToString (); + }; + + // There's no focus change event, so this is a bit of a hack. + menuBar.LayoutComplete += (s, e) => { + _focusedView.Text = Application.Top.MostFocused?.ToString() ?? "None"; + }; + + var openBtn = new Button () { + X = Pos.Center (), + Y = 4, + Text = "_Open Menu", + IsDefault = true + }; + openBtn.Clicked += (s, e) => { + menuBar.OpenMenu (); + }; + Application.Top.Add (openBtn); + + var hideBtn = new Button () { + X = Pos.Center (), + Y = Pos.Bottom(openBtn), + Text = "Toggle Menu._Visible", + }; + hideBtn.Clicked += (s, e) => { + menuBar.Visible = !menuBar.Visible; + }; + Application.Top.Add (hideBtn); + + var enableBtn = new Button () { + X = Pos.Center (), + Y = Pos.Bottom (hideBtn), + Text = "_Toggle Menu.Enable", + }; + enableBtn.Clicked += (s, e) => { + menuBar.Enabled = !menuBar.Enabled; + }; + Application.Top.Add (enableBtn); + + Application.Top.Add (menuBar); + } + + void SetCurrentMenuBarItem (MenuItem mbi) + { + _currentMenuBarItem.Text = mbi != null ? mbi.Title : "Closed"; + } + + void SetCurrentMenuItem (MenuItem mi) + { + _currentMenuItem.Text = mi != null ? mi.Title : "None"; + } + +} \ No newline at end of file diff --git a/UICatalog/Scenarios/Notepad.cs b/UICatalog/Scenarios/Notepad.cs index c521d896a0..56e13da560 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -6,7 +6,7 @@ namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "Notepad", Description: "Multi-tab text editor using the TabView control.")] - [ScenarioCategory ("Controls"), ScenarioCategory ("TabView"), ScenarioCategory("TextView")] + [ScenarioCategory ("Controls"), ScenarioCategory ("TabView"), ScenarioCategory ("TextView")] public class Notepad : Scenario { TabView tabView; @@ -25,13 +25,14 @@ public override void Setup () { var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_New", "", () => New()), + new MenuItem ("_New", "", () => New(), null, null, KeyCode.N | KeyCode.CtrlMask | KeyCode.AltMask), new MenuItem ("_Open", "", () => Open()), new MenuItem ("_Save", "", () => Save()), new MenuItem ("Save _As", "", () => SaveAs()), new MenuItem ("_Close", "", () => Close()), new MenuItem ("_Quit", "", () => Quit()), - }) + }), + new MenuBarItem ("_About", "", () => MessageBox.Query("Notepad", "About Notepad...", "Ok")) }); Application.Top.Add (menu); @@ -41,18 +42,18 @@ public override void Setup () tabView.ApplyStyleChanges (); // Start with only a single view but support splitting to show side by side - var split = new TileView(1) { + var split = new TileView (1) { X = 0, Y = 1, Width = Dim.Fill (), Height = Dim.Fill (1), }; - split.Tiles.ElementAt(0).ContentView.Add (tabView); + split.Tiles.ElementAt (0).ContentView.Add (tabView); split.LineStyle = LineStyle.None; Application.Top.Add (split); - lenStatusItem = new StatusItem (Key.CharMask, "Len: ", null); + lenStatusItem = new StatusItem (KeyCode.CharMask, "Len: ", null); var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), @@ -60,8 +61,8 @@ public override void Setup () //new StatusItem(Key.CtrlMask | Key.N, "~^O~ Open", () => Open()), //new StatusItem(Key.CtrlMask | Key.N, "~^N~ New", () => New()), - new StatusItem(Key.CtrlMask | Key.S, "~^S~ Save", () => Save()), - new StatusItem(Key.CtrlMask | Key.W, "~^W~ Close", () => Close()), + new StatusItem(KeyCode.CtrlMask | KeyCode.S, "~^S~ Save", () => Save()), + new StatusItem(KeyCode.CtrlMask | KeyCode.W, "~^W~ Close", () => Close()), lenStatusItem, }); focusedTabView = tabView; @@ -81,7 +82,7 @@ private void TabView_SelectedTabChanged (object sender, TabChangedEventArgs e) private void TabView_TabClicked (object sender, TabMouseEventArgs e) { // we are only interested in right clicks - if(!e.MouseEvent.Flags.HasFlag(MouseFlags.Button3Clicked)) { + if (!e.MouseEvent.Flags.HasFlag (MouseFlags.Button3Clicked)) { return; } @@ -108,9 +109,9 @@ private void TabView_TabClicked (object sender, TabMouseEventArgs e) }); } - ((View)sender).BoundsToScreen (e.MouseEvent.X, e.MouseEvent.Y, out int screenX, out int screenY,true); + ((View)sender).BoundsToScreen (e.MouseEvent.X, e.MouseEvent.Y, out int screenX, out int screenY, true); - var contextMenu = new ContextMenu (screenX,screenY, items); + var contextMenu = new ContextMenu (screenX, screenY, items); contextMenu.Show (); e.MouseEvent.Handled = true; @@ -118,48 +119,46 @@ private void TabView_TabClicked (object sender, TabMouseEventArgs e) private void SplitUp (TabView sender, OpenedFile tab) { - Split(0, Orientation.Horizontal,sender,tab); + Split (0, Orientation.Horizontal, sender, tab); } private void SplitDown (TabView sender, OpenedFile tab) { - Split(1, Orientation.Horizontal,sender,tab); - + Split (1, Orientation.Horizontal, sender, tab); + } private void SplitLeft (TabView sender, OpenedFile tab) { - Split(0, Orientation.Vertical,sender,tab); + Split (0, Orientation.Vertical, sender, tab); } private void SplitRight (TabView sender, OpenedFile tab) { - Split(1, Orientation.Vertical,sender,tab); + Split (1, Orientation.Vertical, sender, tab); } - private void Split (int offset, Orientation orientation,TabView sender, OpenedFile tab) + private void Split (int offset, Orientation orientation, TabView sender, OpenedFile tab) { - + var split = (TileView)sender.SuperView.SuperView; - var tileIndex = split.IndexOf(sender); + var tileIndex = split.IndexOf (sender); - if(tileIndex == -1) - { + if (tileIndex == -1) { return; } - if(orientation != split.Orientation) - { - split.TrySplitTile(tileIndex,1,out split); + if (orientation != split.Orientation) { + split.TrySplitTile (tileIndex, 1, out split); split.Orientation = orientation; tileIndex = 0; } - var newTile = split.InsertTile(tileIndex + offset); + var newTile = split.InsertTile (tileIndex + offset); var newTabView = CreateNewTabView (); tab.CloneTo (newTabView); - newTile.ContentView.Add(newTabView); + newTile.ContentView.Add (newTabView); - newTabView.EnsureFocus(); - newTabView.FocusFirst(); - newTabView.FocusNext(); + newTabView.EnsureFocus (); + newTabView.FocusFirst (); + newTabView.FocusNext (); } private TabView CreateNewTabView () @@ -207,7 +206,7 @@ private void Close (TabView tv, Tab tabToClose) } if (result == 0) { - if(tab.File == null) { + if (tab.File == null) { SaveAs (); } else { tab.Save (); @@ -220,20 +219,20 @@ private void Close (TabView tv, Tab tabToClose) tab.View.Dispose (); focusedTabView = tv; - if(tv.Tabs.Count == 0) { + if (tv.Tabs.Count == 0) { var split = (TileView)tv.SuperView.SuperView; // if it is the last TabView on screen don't drop it or we will // be unable to open new docs! - if(split.IsRootTileView() && split.Tiles.Count == 1) { + if (split.IsRootTileView () && split.Tiles.Count == 1) { return; } var tileIndex = split.IndexOf (tv); split.RemoveTile (tileIndex); - if(split.Tiles.Count == 0) { + if (split.Tiles.Count == 0) { var parent = split.GetParentTileView (); if (parent == null) { @@ -315,8 +314,8 @@ public bool SaveAs () if (string.IsNullOrWhiteSpace (fd.Path)) { return false; } - - if(fd.Canceled) { + + if (fd.Canceled) { return false; } @@ -338,8 +337,8 @@ private class OpenedFile : Tab { public bool UnsavedChanges => !string.Equals (SavedText, View.Text); - public OpenedFile (TabView parent, string name, FileInfo file) - : base (name, CreateTextView(file)) + public OpenedFile (TabView parent, string name, FileInfo file) + : base (name, CreateTextView (file)) { File = file; @@ -363,7 +362,7 @@ private void RegisterTextViewEvents (TabView parent) parent.SetNeedsDisplay (); } } else { - + if (Text.EndsWith ('*')) { Text = Text.TrimEnd ('*'); @@ -376,8 +375,8 @@ private void RegisterTextViewEvents (TabView parent) private static View CreateTextView (FileInfo file) { string initialText = string.Empty; - if(file != null && file.Exists) { - + if (file != null && file.Exists) { + initialText = System.IO.File.ReadAllText (file.FullName); } @@ -390,9 +389,9 @@ private static View CreateTextView (FileInfo file) AllowsTab = false, }; } - public OpenedFile CloneTo(TabView other) + public OpenedFile CloneTo (TabView other) { - var newTab = new OpenedFile (other, base.Text.ToString(), File); + var newTab = new OpenedFile (other, base.Text.ToString (), File); other.AddTab (newTab, true); return newTab; } diff --git a/UICatalog/Scenarios/SendKeys.cs b/UICatalog/Scenarios/SendKeys.cs index dd9eef8ddd..20c062ce0b 100644 --- a/UICatalog/Scenarios/SendKeys.cs +++ b/UICatalog/Scenarios/SendKeys.cs @@ -57,17 +57,17 @@ public override void Setup () var IsAlt = false; var IsCtrl = false; - txtResult.KeyPressed += (s, e) => { - rKeys += (char)e.KeyEvent.Key; - if (!IsShift && e.KeyEvent.IsShift) { + txtResult.KeyDown += (s, e) => { + rKeys += (char)e.KeyCode; + if (!IsShift && e.IsShift) { rControlKeys += " Shift "; IsShift = true; } - if (!IsAlt && e.KeyEvent.IsAlt) { + if (!IsAlt && e.IsAlt) { rControlKeys += " Alt "; IsAlt = true; } - if (!IsCtrl && e.KeyEvent.IsCtrl) { + if (!IsCtrl && e.IsCtrl) { rControlKeys += " Ctrl "; IsCtrl = true; } @@ -116,8 +116,8 @@ void ProcessInput () button.Clicked += (s,e) => ProcessInput (); - Win.KeyPressed += (s, e) => { - if (e.KeyEvent.Key == Key.Enter) { + Win.KeyDown += (s, e) => { + if (e.KeyCode == KeyCode.Enter) { ProcessInput (); e.Handled = true; } diff --git a/UICatalog/Scenarios/SingleBackgroundWorker.cs b/UICatalog/Scenarios/SingleBackgroundWorker.cs index 80b3693e65..2ab33ad5ae 100644 --- a/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -28,16 +28,16 @@ public MainApp () { var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_Options", new MenuItem [] { - new MenuItem ("_Run Worker", "", () => RunWorker(), null, null, Key.CtrlMask | Key.R), + new MenuItem ("_Run Worker", "", () => RunWorker(), null, null, KeyCode.CtrlMask | KeyCode.R), null, - new MenuItem ("_Quit", "", () => Application.RequestStop(), null, null, Key.CtrlMask | Key.Q) + new MenuItem ("_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(Key.CtrlMask | Key.P, "~^R~ Run Worker", () => RunWorker()) + new StatusItem(KeyCode.CtrlMask | KeyCode.P, "~^R~ Run Worker", () => RunWorker()) }); Add (statusBar); @@ -133,10 +133,10 @@ public class StagingUIController : Window { public StagingUIController (DateTime? start, List list) { top = new Toplevel (Application.Top.Frame); - top.KeyPressed += (s,e) => { + top.KeyDown += (s,e) => { // Prevents Ctrl+Q from closing this. // Only Ctrl+C is allowed. - if (e.KeyEvent.Key == Application.QuitKey) { + if (e == Application.QuitKey) { e.Handled = true; } }; @@ -149,13 +149,13 @@ bool Close () var menu = new MenuBar (new MenuBarItem [] { new MenuBarItem ("_Stage", new MenuItem [] { - new MenuItem ("_Close", "", () => { if (Close()) { Application.RequestStop(); } }, null, null, Key.CtrlMask | Key.C) + new MenuItem ("_Close", "", () => { if (Close()) { Application.RequestStop(); } }, null, null, KeyCode.CtrlMask | KeyCode.C) }) }); top.Add (menu); var statusBar = new StatusBar (new [] { - new StatusItem(Key.CtrlMask | Key.C, "~^C~ Close", () => { if (Close()) { Application.RequestStop(); } }), + new StatusItem(KeyCode.CtrlMask | KeyCode.C, "~^C~ Close", () => { if (Close()) { Application.RequestStop(); } }), }); top.Add (statusBar); diff --git a/UICatalog/Scenarios/Snake.cs b/UICatalog/Scenarios/Snake.cs index 327480ba8c..0c8f4cec9e 100644 --- a/UICatalog/Scenarios/Snake.cs +++ b/UICatalog/Scenarios/Snake.cs @@ -124,21 +124,23 @@ public override void OnDrawContent (Rect contentArea) AddRune (State.Apple.X, State.Apple.Y, _appleRune); Driver.SetAttribute (white); } - public override bool OnKeyDown (KeyEvent keyEvent) + + // BUGBUG: Should (can) this use key bindings instead. + public override bool OnKeyDown (Key keyEvent) { - if (keyEvent.Key == Key.CursorUp) { + if (keyEvent.KeyCode == KeyCode.CursorUp) { State.PlannedDirection = Direction.Up; return true; } - if (keyEvent.Key == Key.CursorDown) { + if (keyEvent.KeyCode == KeyCode.CursorDown) { State.PlannedDirection = Direction.Down; return true; } - if (keyEvent.Key == Key.CursorLeft) { + if (keyEvent.KeyCode == KeyCode.CursorLeft) { State.PlannedDirection = Direction.Left; return true; } - if (keyEvent.Key == Key.CursorRight) { + if (keyEvent.KeyCode == KeyCode.CursorRight) { State.PlannedDirection = Direction.Right; return true; } diff --git a/UICatalog/Scenarios/TableEditor.cs b/UICatalog/Scenarios/TableEditor.cs index bf477e8eac..5f9cf053f0 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -100,9 +100,9 @@ public override void Setup () Application.Top.Add (menu); var statusBar = new StatusBar (new StatusItem [] { - new StatusItem(Key.F2, "~F2~ OpenExample", () => OpenExample(true)), - new StatusItem(Key.F3, "~F3~ CloseExample", () => CloseExample()), - new StatusItem(Key.F4, "~F4~ OpenSimple", () => OpenSimple(true)), + new StatusItem(KeyCode.F2, "~F2~ OpenExample", () => OpenExample(true)), + new StatusItem(KeyCode.F3, "~F3~ CloseExample", () => CloseExample()), + new StatusItem(KeyCode.F4, "~F4~ OpenSimple", () => OpenSimple(true)), new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), }); Application.Top.Add (statusBar); @@ -122,7 +122,7 @@ public override void Setup () tableView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; }; tableView.CellActivated += EditCurrentCell; - tableView.KeyPressed += TableViewKeyPress; + tableView.KeyDown += TableViewKeyPress; SetupScrollBar (); @@ -170,7 +170,7 @@ public override void Setup () } }; - tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); + tableView.KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); } private void ShowAllColumns () @@ -386,13 +386,13 @@ private void SetupScrollBar () } - private void TableViewKeyPress (object sender, KeyEventEventArgs e) + private void TableViewKeyPress (object sender, Key e) { if(currentTable == null) { return; } - if (e.KeyEvent.Key == Key.DeleteChar) { + if (e.KeyCode == KeyCode.DeleteChar) { if (tableView.FullRowSelect) { // Delete button deletes all rows when in full row mode diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs index 6891b2ba2d..48320ee4b9 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -119,15 +119,15 @@ void TextView_DrawContent (object sender, DrawEventArgs e) } }; - Key keyTab = textView.GetKeyFromCommand (Command.Tab); - Key keyBackTab = textView.GetKeyFromCommand (Command.BackTab); + var keyTab = textView.KeyBindings.GetKeyFromCommands (Command.Tab); + var keyBackTab = textView.KeyBindings.GetKeyFromCommands (Command.BackTab); chxCaptureTabs.Toggled += (s, e) => { if (e.NewValue == true) { - textView.AddKeyBinding (keyTab, Command.Tab); - textView.AddKeyBinding (keyBackTab, Command.BackTab); + textView.KeyBindings.Add (keyTab, Command.Tab); + textView.KeyBindings.Add (keyBackTab, Command.BackTab); } else { - textView.ClearKeyBinding (keyTab); - textView.ClearKeyBinding (keyBackTab); + textView.KeyBindings.Remove (keyTab); + textView.KeyBindings.Remove (keyBackTab); } textView.AllowsTab = (bool)e.NewValue; }; diff --git a/UICatalog/Scenarios/TextViewAutocompletePopup.cs b/UICatalog/Scenarios/TextViewAutocompletePopup.cs index 4efa3a967d..50f5ca3434 100644 --- a/UICatalog/Scenarios/TextViewAutocompletePopup.cs +++ b/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -87,8 +87,8 @@ public override void Setup () var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} to Quit", () => Quit()), - siMultiline = new StatusItem(Key.Null, "", null), - siWrap = new StatusItem(Key.Null, "", null) + siMultiline = new StatusItem(KeyCode.Null, "", null), + siWrap = new StatusItem(KeyCode.Null, "", null) }); Application.Top.Add (statusBar); diff --git a/UICatalog/Scenarios/TreeViewFileSystem.cs b/UICatalog/Scenarios/TreeViewFileSystem.cs index 7e4c722426..1c13c38419 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -95,7 +95,7 @@ public override void Setup () Win.Add (_detailsFrame); treeViewFiles.MouseClick += TreeViewFiles_MouseClick; - treeViewFiles.KeyPressed += TreeViewFiles_KeyPress; + treeViewFiles.KeyDown += TreeViewFiles_KeyPress; treeViewFiles.SelectionChanged += TreeViewFiles_SelectionChanged; SetupFileTree (); @@ -158,9 +158,9 @@ private void TreeViewFiles_DrawLine (object sender, DrawTreeViewLineEventArgs Application.RequestStop()), - new StatusItem (Key.Unknown, "~F2~ Создать", null), - new StatusItem(Key.Unknown, "~F3~ Со_хранить", null), + new StatusItem (KeyCode.Unknown, "~F2~ Создать", null), + new StatusItem(KeyCode.Unknown, "~F3~ Со_хранить", null), }); Application.Top.Add (statusBar); diff --git a/UICatalog/Scenarios/VkeyPacketSimulator.cs b/UICatalog/Scenarios/VkeyPacketSimulator.cs index f1d940f6e6..4bb4fdd2e4 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Terminal.Gui; +using Terminal.Gui.ConsoleDrivers; namespace UICatalog.Scenarios { [ScenarioMetadata (Name: "VkeyPacketSimulator", Description: "Simulates the Virtual Key Packet")] @@ -44,6 +45,7 @@ public override void Setup () Win.Add (inputVerticalRuler); var tvInput = new TextView { + Title = "Input", X = 1, Y = Pos.Bottom (inputHorizontalRuler), Width = Dim.Fill (), @@ -81,6 +83,7 @@ public override void Setup () Win.Add (outputVerticalRuler); var tvOutput = new TextView { + Title = "Output", X = 1, Y = Pos.Bottom (outputHorizontalRuler), Width = Dim.Fill (), @@ -88,24 +91,24 @@ public override void Setup () ReadOnly = true }; + // Detect unknown keys and reject them. tvOutput.KeyDown += (s, e) => { - //System.Diagnostics.Debug.WriteLine ($"Output - KeyDown: {e.KeyEvent.Key}"); - e.Handled = true; - if (e.KeyEvent.Key == Key.Unknown) { + //System.Diagnostics.Debug.WriteLine ($"Output - KeyDown: {e.Key}"); + //e.Handled = true; + if (e.KeyCode == KeyCode.Unknown) { _wasUnknown = true; } }; - tvOutput.KeyPressed += (s, e) => { + tvOutput.KeyUp += (s, e) => { //System.Diagnostics.Debug.WriteLine ($"Output - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}"); if (_outputStarted && _keyboardStrokes.Count > 0) { - var ev = ShortcutHelper.GetModifiersKey (e.KeyEvent); - //System.Diagnostics.Debug.WriteLine ($"Output - KeyPress: {ev}"); - if (!tvOutput.ProcessKey (e.KeyEvent)) { - Application.Invoke (() => { - MessageBox.Query ("Keys", $"'{ShortcutHelper.GetShortcutTag (ev)}' pressed!", "Ok"); - }); - } + //// TODO: Tig: I don't understand what this is trying to do + //if (!tvOutput.ProcessKeyDown (e)) { + // Application.Invoke (() => { + // MessageBox.Query ("Keys", $"'{KeyEventArgs.ToString (e.ConsoleDriverKey, MenuBar.ShortcutDelimiter)}' pressed!", "Ok"); + // }); + //} e.Handled = true; _stopOutput.Set (); } @@ -114,49 +117,28 @@ public override void Setup () Win.Add (tvOutput); - tvInput.KeyDown += (s, e) => { - //System.Diagnostics.Debug.WriteLine ($"Input - KeyDown: {e.KeyEvent.Key}"); - e.Handled = true; - if (e.KeyEvent.Key == Key.Unknown) { - _wasUnknown = true; - } - }; + Key unknownChar = null; - KeyEventEventArgs unknownChar = null; + tvInput.KeyUp += (s, e) => { + //System.Diagnostics.Debug.WriteLine ($"Input - KeyUp: {e.Key}"); + //var ke = e; - tvInput.KeyPressed += (s, e) => { - if (e.KeyEvent.Key == (Key.Q | Key.CtrlMask)) { - Application.RequestStop (); - return; - } - if (e.KeyEvent.Key == Key.Unknown) { + if (e.KeyCode == KeyCode.Unknown) { _wasUnknown = true; e.Handled = true; return; } if (_wasUnknown && _keyboardStrokes.Count == 1) { _wasUnknown = false; - } else if (_wasUnknown && char.IsLetter ((char)e.KeyEvent.Key)) { + } else if (_wasUnknown && char.IsLetter ((char)e.KeyCode)) { _wasUnknown = false; - } else if (!_wasUnknown && _keyboardStrokes.Count > 0) { - e.Handled = true; - return; } if (_keyboardStrokes.Count == 0) { AddKeyboardStrokes (e); } else { _keyboardStrokes.Insert (0, 0); } - var ev = ShortcutHelper.GetModifiersKey (e.KeyEvent); - //System.Diagnostics.Debug.WriteLine ($"Input - KeyPress: {ev}"); - //System.Diagnostics.Debug.WriteLine ($"Input - KeyPress - _keyboardStrokes: {_keyboardStrokes.Count}"); - }; - - tvInput.KeyUp += (s, e) => { - //System.Diagnostics.Debug.WriteLine ($"Input - KeyUp: {e.KeyEvent.Key}"); - //var ke = e.KeyEvent; - var ke = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (_wasUnknown && (int)ke - (int)(ke & (Key.AltMask | Key.CtrlMask | Key.ShiftMask)) != 0) { + if (_wasUnknown && (int)e.KeyCode - (int)(e.KeyCode & (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.ShiftMask)) != 0) { unknownChar = e; } e.Handled = true; @@ -170,18 +152,18 @@ public override void Setup () while (_outputStarted) { try { ConsoleModifiers mod = new ConsoleModifiers (); - if (ke.HasFlag (Key.ShiftMask)) { + if (e.KeyCode.HasFlag (KeyCode.ShiftMask)) { mod |= ConsoleModifiers.Shift; } - if (ke.HasFlag (Key.AltMask)) { + if (e.KeyCode.HasFlag (KeyCode.AltMask)) { mod |= ConsoleModifiers.Alt; } - if (ke.HasFlag (Key.CtrlMask)) { + if (e.KeyCode.HasFlag (KeyCode.CtrlMask)) { mod |= ConsoleModifiers.Control; } for (int i = 0; i < _keyboardStrokes.Count; i++) { - var consoleKey = ConsoleKeyMapping.GetConsoleKeyFromKey ((uint)_keyboardStrokes [i], mod, out _, out _); - Application.Driver.SendKeys ((char)consoleKey, ConsoleKey.Packet, mod.HasFlag (ConsoleModifiers.Shift), + var consoleKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey ((uint)_keyboardStrokes [i], mod, out _); + Application.Driver.SendKeys (consoleKeyInfo.KeyChar, ConsoleKey.Packet, mod.HasFlag (ConsoleModifiers.Shift), mod.HasFlag (ConsoleModifiers.Alt), mod.HasFlag (ConsoleModifiers.Control)); } //} @@ -207,13 +189,13 @@ public override void Setup () } }; - btnInput.Clicked += (s,e) => { + btnInput.Clicked += (s, e) => { if (!tvInput.HasFocus && _keyboardStrokes.Count == 0) { tvInput.SetFocus (); } }; - btnOutput.Clicked += (s,e) => { + btnOutput.Clicked += (s, e) => { if (!tvOutput.HasFocus && _keyboardStrokes.Count == 0) { tvOutput.SetFocus (); } @@ -232,21 +214,10 @@ void Win_LayoutComplete (object sender, LayoutEventArgs obj) Win.LayoutComplete += Win_LayoutComplete; } - private void AddKeyboardStrokes (KeyEventEventArgs e) + private void AddKeyboardStrokes (Key e) { - var ke = e.KeyEvent; - var km = new KeyModifiers (); - if (ke.IsShift) { - km.Shift = true; - } - if (ke.IsAlt) { - km.Alt = true; - } - if (ke.IsCtrl) { - km.Ctrl = true; - } - var keyChar = ke.KeyValue; - var mK = (int)((Key)ke.KeyValue & (Key.AltMask | Key.CtrlMask | Key.ShiftMask)); + var keyChar = (int)e.KeyCode; + var mK = (int)(e.KeyCode & (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.ShiftMask)); keyChar &= ~mK; _keyboardStrokes.Add (keyChar); } diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index 3c0a3cb256..b8171714f9 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -1,6 +1,5 @@ global using CM = Terminal.Gui.ConfigurationManager; global using Attribute = Terminal.Gui.Attribute; - using System; using System.Collections.Generic; using System.Diagnostics; @@ -19,856 +18,821 @@ #nullable enable -namespace UICatalog { - /// - /// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios. - /// - /// - /// - /// UI Catalog attempts to satisfy the following goals: - /// - /// - /// - /// - /// - /// Be an easy to use showcase for Terminal.Gui concepts and features. - /// - /// - /// - /// - /// Provide sample code that illustrates how to properly implement said concepts & features. - /// - /// - /// - /// - /// Make it easy for contributors to add additional samples in a structured way. - /// - /// - /// - /// - /// - /// See the project README for more details (https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog/README.md). - /// - /// - class UICatalogApp { - [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true), JsonPropertyName ("UICatalog.StatusBar")] - public static bool ShowStatusBar { get; set; } = true; - - static readonly FileSystemWatcher _currentDirWatcher = new FileSystemWatcher (); - static readonly FileSystemWatcher _homeDirWatcher = new FileSystemWatcher (); - - static void Main (string [] args) - { - Console.OutputEncoding = Encoding.Default; - - if (Debugger.IsAttached) { - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); - } - - _scenarios = Scenario.GetScenarios (); - _categories = Scenario.GetAllCategories (); - - if (args.Length > 0 && args.Contains ("-usc")) { - _useSystemConsole = true; - args = args.Where (val => val != "-usc").ToArray (); - } +namespace UICatalog; + +/// +/// UI Catalog is a comprehensive sample library for Terminal.Gui. It provides a simple UI for adding to the catalog of scenarios. +/// +/// +/// +/// UI Catalog attempts to satisfy the following goals: +/// +/// +/// +/// +/// +/// Be an easy to use showcase for Terminal.Gui concepts and features. +/// +/// +/// +/// +/// Provide sample code that illustrates how to properly implement said concepts & features. +/// +/// +/// +/// +/// Make it easy for contributors to add additional samples in a structured way. +/// +/// +/// +/// +/// +/// See the project README for more details (https://github.com/gui-cs/Terminal.Gui/tree/master/UICatalog/README.md). +/// +/// +class UICatalogApp { + [SerializableConfigurationProperty (Scope = typeof (AppScope), OmitClassName = true)] [JsonPropertyName ("UICatalog.StatusBar")] + public static bool ShowStatusBar { get; set; } = true; + + static readonly FileSystemWatcher _currentDirWatcher = new (); + static readonly FileSystemWatcher _homeDirWatcher = new (); + + static void Main (string [] args) + { + Console.OutputEncoding = Encoding.Default; + + if (Debugger.IsAttached) { + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + } - StartConfigFileWatcher (); + _scenarios = Scenario.GetScenarios (); + _categories = Scenario.GetAllCategories (); - // If a Scenario name has been provided on the commandline - // run it and exit when done. - if (args.Length > 0) { - _topLevelColorScheme = "Base"; + if (args.Length > 0 && args.Contains ("-usc")) { + _useSystemConsole = true; + args = args.Where (val => val != "-usc").ToArray (); + } - var item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!; - Application.UseSystemConsole = _useSystemConsole; - Application.Init (); - _selectedScenario.Theme = _cachedTheme; - _selectedScenario.TopLevelColorScheme = _topLevelColorScheme; - _selectedScenario.Init (); - _selectedScenario.Setup (); - _selectedScenario.Run (); - _selectedScenario.Dispose (); - _selectedScenario = null; - Application.Shutdown (); - VerifyObjectsWereDisposed (); - return; - } + StartConfigFileWatcher (); - _aboutMessage = new StringBuilder (); - _aboutMessage.AppendLine (@"A comprehensive sample library for"); - _aboutMessage.AppendLine (@""); - _aboutMessage.AppendLine (@" _______ _ _ _____ _ "); - _aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) "); - _aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); - _aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | "); - _aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | "); - _aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| "); - _aboutMessage.AppendLine (@""); - _aboutMessage.AppendLine (@"v2 - Work in Progress"); - _aboutMessage.AppendLine (@""); - _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); - - while (RunUICatalogTopLevel () is { } scenario) { - VerifyObjectsWereDisposed (); - CM.Themes!.Theme = _cachedTheme!; - CM.Apply (); - scenario.Theme = _cachedTheme; - scenario.TopLevelColorScheme = _topLevelColorScheme; - scenario.Init (); - scenario.Setup (); - scenario.Run (); - scenario.Dispose (); - - // This call to Application.Shutdown brackets the Application.Init call - // made by Scenario.Init() above - Application.Shutdown (); - - VerifyObjectsWereDisposed (); - } + // If a Scenario name has been provided on the commandline + // run it and exit when done. + if (args.Length > 0) { + _topLevelColorScheme = "Base"; - StopConfigFileWatcher (); + int item = _scenarios.FindIndex (s => s.GetName ().Equals (args [0], StringComparison.OrdinalIgnoreCase)); + _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios [item].GetType ())!; + Application.UseSystemConsole = _useSystemConsole; + Application.Init (); + _selectedScenario.Theme = _cachedTheme; + _selectedScenario.TopLevelColorScheme = _topLevelColorScheme; + _selectedScenario.Init (); + _selectedScenario.Setup (); + _selectedScenario.Run (); + _selectedScenario.Dispose (); + _selectedScenario = null; + Application.Shutdown (); VerifyObjectsWereDisposed (); + return; } - private static void StopConfigFileWatcher () - { - _currentDirWatcher.EnableRaisingEvents = false; - _currentDirWatcher.Changed -= ConfigFileChanged; - _currentDirWatcher.Created -= ConfigFileChanged; + _aboutMessage = new StringBuilder (); + _aboutMessage.AppendLine (@"A comprehensive sample library for"); + _aboutMessage.AppendLine (@""); + _aboutMessage.AppendLine (@" _______ _ _ _____ _ "); + _aboutMessage.AppendLine (@" |__ __| (_) | | / ____| (_) "); + _aboutMessage.AppendLine (@" | | ___ _ __ _ __ ___ _ _ __ __ _| || | __ _ _ _ "); + _aboutMessage.AppendLine (@" | |/ _ \ '__| '_ ` _ \| | '_ \ / _` | || | |_ | | | | | "); + _aboutMessage.AppendLine (@" | | __/ | | | | | | | | | | | (_| | || |__| | |_| | | "); + _aboutMessage.AppendLine (@" |_|\___|_| |_| |_| |_|_|_| |_|\__,_|_(_)_____|\__,_|_| "); + _aboutMessage.AppendLine (@""); + _aboutMessage.AppendLine (@"v2 - Work in Progress"); + _aboutMessage.AppendLine (@""); + _aboutMessage.AppendLine (@"https://github.com/gui-cs/Terminal.Gui"); + + while (RunUICatalogTopLevel () is { } scenario) { + VerifyObjectsWereDisposed (); + Themes!.Theme = _cachedTheme!; + Apply (); + scenario.Theme = _cachedTheme; + scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.Init (); + scenario.Setup (); + scenario.Run (); + scenario.Dispose (); + + // This call to Application.Shutdown brackets the Application.Init call + // made by Scenario.Init() above + Application.Shutdown (); - _homeDirWatcher.EnableRaisingEvents = false; - _homeDirWatcher.Changed -= ConfigFileChanged; - _homeDirWatcher.Created -= ConfigFileChanged; + VerifyObjectsWereDisposed (); } - private static void StartConfigFileWatcher () - { - // Setup a file system watcher for `./.tui/` - _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite; - var f = new FileInfo (Assembly.GetExecutingAssembly ().Location); - var tuiDir = Path.Combine (f.Directory!.FullName, ".tui"); - - if (!Directory.Exists (tuiDir)) { - Directory.CreateDirectory (tuiDir); - } - _currentDirWatcher.Path = tuiDir; - _currentDirWatcher.Filter = "*config.json"; + StopConfigFileWatcher (); + VerifyObjectsWereDisposed (); + } - // Setup a file system watcher for `~/.tui/` - _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite; - f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); - tuiDir = Path.Combine (f.FullName, ".tui"); + static void StopConfigFileWatcher () + { + _currentDirWatcher.EnableRaisingEvents = false; + _currentDirWatcher.Changed -= ConfigFileChanged; + _currentDirWatcher.Created -= ConfigFileChanged; - if (!Directory.Exists (tuiDir)) { - Directory.CreateDirectory (tuiDir); - } - _homeDirWatcher.Path = tuiDir; - _homeDirWatcher.Filter = "*config.json"; + _homeDirWatcher.EnableRaisingEvents = false; + _homeDirWatcher.Changed -= ConfigFileChanged; + _homeDirWatcher.Created -= ConfigFileChanged; + } - _currentDirWatcher.Changed += ConfigFileChanged; - //_currentDirWatcher.Created += ConfigFileChanged; - _currentDirWatcher.EnableRaisingEvents = true; + static void StartConfigFileWatcher () + { + // Setup a file system watcher for `./.tui/` + _currentDirWatcher.NotifyFilter = NotifyFilters.LastWrite; + var f = new FileInfo (Assembly.GetExecutingAssembly ().Location); + string tuiDir = Path.Combine (f.Directory!.FullName, ".tui"); - _homeDirWatcher.Changed += ConfigFileChanged; - //_homeDirWatcher.Created += ConfigFileChanged; - _homeDirWatcher.EnableRaisingEvents = true; + if (!Directory.Exists (tuiDir)) { + Directory.CreateDirectory (tuiDir); } + _currentDirWatcher.Path = tuiDir; + _currentDirWatcher.Filter = "*config.json"; - private static void ConfigFileChanged (object sender, FileSystemEventArgs e) - { - if (Application.Top == null) { - return; - } + // Setup a file system watcher for `~/.tui/` + _homeDirWatcher.NotifyFilter = NotifyFilters.LastWrite; + f = new FileInfo (Environment.GetFolderPath (Environment.SpecialFolder.UserProfile)); + tuiDir = Path.Combine (f.FullName, ".tui"); - // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it. - //Thread.Sleep (500); - CM.Load (); - CM.Apply (); + if (!Directory.Exists (tuiDir)) { + Directory.CreateDirectory (tuiDir); } + _homeDirWatcher.Path = tuiDir; + _homeDirWatcher.Filter = "*config.json"; - /// - /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the - /// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top. - /// When the Scenario exits, this function exits. - /// - /// - static Scenario RunUICatalogTopLevel () - { - Application.UseSystemConsole = _useSystemConsole; + _currentDirWatcher.Changed += ConfigFileChanged; + //_currentDirWatcher.Created += ConfigFileChanged; + _currentDirWatcher.EnableRaisingEvents = true; - // Run UI Catalog UI. When it exits, if _selectedScenario is != null then - // a Scenario was selected. Otherwise, the user wants to quit UI Catalog. - Application.Init (); + _homeDirWatcher.Changed += ConfigFileChanged; + //_homeDirWatcher.Created += ConfigFileChanged; + _homeDirWatcher.EnableRaisingEvents = true; + } - if (_cachedTheme is null) { - _cachedTheme = CM.Themes?.Theme; - } else { - CM.Themes!.Theme = _cachedTheme; - CM.Apply (); - } + static void ConfigFileChanged (object sender, FileSystemEventArgs e) + { + if (Application.Top == null) { + return; + } - Application.Run (); - Application.Shutdown (); + // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it. + //Thread.Sleep (500); + Load (); + Apply (); + } - return _selectedScenario!; + /// + /// Shows the UI Catalog selection UI. When the user selects a Scenario to run, the + /// UI Catalog main app UI is killed and the Scenario is run as though it were Application.Top. + /// When the Scenario exits, this function exits. + /// + /// + static Scenario RunUICatalogTopLevel () + { + Application.UseSystemConsole = _useSystemConsole; + + // Run UI Catalog UI. When it exits, if _selectedScenario is != null then + // a Scenario was selected. Otherwise, the user wants to quit UI Catalog. + Application.Init (); + + if (_cachedTheme is null) { + _cachedTheme = Themes?.Theme; + } else { + Themes!.Theme = _cachedTheme; + Apply (); } - static List? _scenarios; - static List? _categories; + Application.Run (); + Application.Shutdown (); - // When a scenario is run, the main app is killed. These items - // are therefore cached so that when the scenario exits the - // main app UI can be restored to previous state - static int _cachedScenarioIndex = 0; - static int _cachedCategoryIndex = 0; - static string? _cachedTheme = string.Empty; + return _selectedScenario!; + } - static StringBuilder? _aboutMessage = null; + static List? _scenarios; + static List? _categories; - // If set, holds the scenario the user selected - static Scenario? _selectedScenario = null; + // When a scenario is run, the main app is killed. These items + // are therefore cached so that when the scenario exits the + // main app UI can be restored to previous state + static int _cachedScenarioIndex = 0; + static int _cachedCategoryIndex = 0; + static string? _cachedTheme = string.Empty; - static bool _useSystemConsole = false; - static ConsoleDriver.DiagnosticFlags _diagnosticFlags; - static bool _isFirstRunning = true; - static string _topLevelColorScheme = string.Empty; + static StringBuilder? _aboutMessage = null; - static MenuItem []? _themeMenuItems; - static MenuBarItem? _themeMenuBarItem; + // If set, holds the scenario the user selected + static Scenario? _selectedScenario = null; - /// - /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on - /// the command line) and each time a Scenario ends. - /// - public class UICatalogTopLevel : Toplevel { - public MenuItem? miUseSubMenusSingleFrame; - public MenuItem? miIsMenuBorderDisabled; - public MenuItem? miForce16Colors; - public MenuItem? miIsMouseDisabled; - - public ListView CategoryList; - - // 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 - // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. - public TableView ScenarioList; - private CollectionNavigator _scenarioCollectionNav = new CollectionNavigator (); - - public StatusItem Capslock; - public StatusItem Numlock; - public StatusItem Scrolllock; - public StatusItem DriverName; - public StatusItem OS; - - public UICatalogTopLevel () - { - _themeMenuItems = CreateThemeMenuItems (); - _themeMenuBarItem = new MenuBarItem ("_Themes", _themeMenuItems); - MenuBar = new MenuBar (new MenuBarItem [] { - new MenuBarItem ("_File", new MenuItem [] { - new MenuItem ("_Quit", "Quit UI Catalog", RequestStop, null, null) - }), - _themeMenuBarItem, - new MenuBarItem ("Diag_nostics", CreateDiagnosticMenuItems()), - new MenuBarItem ("_Help", new MenuItem [] { - new MenuItem ("_gui.cs API Overview", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.Gui/docs/overview.html"), null, null, Key.F1), - new MenuItem ("gui.cs _README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, Key.F2), - new MenuItem ("_About...", - "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage!.ToString(), 0, false, "_Ok"), null, null, Key.CtrlMask | Key.A), - }), - }); - - Capslock = new StatusItem (Key.CharMask, "Caps", null); - Numlock = new StatusItem (Key.CharMask, "Num", null); - Scrolllock = new StatusItem (Key.CharMask, "Scroll", null); - DriverName = new StatusItem (Key.CharMask, "Driver:", null); - OS = new StatusItem (Key.CharMask, "OS:", null); - - StatusBar = new StatusBar () { - Visible = UICatalogApp.ShowStatusBar - }; + static bool _useSystemConsole = false; + static ConsoleDriver.DiagnosticFlags _diagnosticFlags; + static bool _isFirstRunning = true; + static string _topLevelColorScheme = string.Empty; - StatusBar.Items = new StatusItem [] { - new StatusItem(Application.QuitKey, $"~{Application.QuitKey} to quit", () => { - if (_selectedScenario is null){ - // This causes GetScenarioToRun to return null - _selectedScenario = null; - RequestStop(); - } else { - _selectedScenario.RequestStop(); - } - }), - new StatusItem(Key.F10, "~F10~ Status Bar", () => { - StatusBar.Visible = !StatusBar.Visible; - //ContentPane!.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); - LayoutSubviews(); - SetSubViewNeedsDisplay(); - }), - DriverName, - OS - }; + static MenuItem []? _themeMenuItems; + static MenuBarItem? _themeMenuBarItem; - // Create the Category list view. This list never changes. - CategoryList = new ListView (_categories) { - X = 0, - Y = 1, - Width = Dim.Percent (30), - Height = Dim.Fill (1), - AllowsMarking = false, - CanFocus = true, - Title = "Categories", - BorderStyle = LineStyle.Single, - SuperViewRendersLineCanvas = true - }; - CategoryList.OpenSelectedItem += (s, a) => { - ScenarioList!.SetFocus (); - }; - CategoryList.SelectedItemChanged += CategoryView_SelectedChanged; - - // Create the scenario list. The contents of the scenario list changes whenever the - // Category list selection changes (to show just the scenarios that belong to the selected - // category). - ScenarioList = new TableView () { - X = Pos.Right (CategoryList) - 1, - Y = 1, - Width = Dim.Fill (0), - Height = Dim.Fill (1), - //AllowsMarking = false, - CanFocus = true, - Title = "Scenarios", - BorderStyle = LineStyle.Single, - SuperViewRendersLineCanvas = true - }; + /// + /// This is the main UI Catalog app view. It is run fresh when the app loads (if a Scenario has not been passed on + /// the command line) and each time a Scenario ends. + /// + public class UICatalogTopLevel : Toplevel { + public MenuItem? miUseSubMenusSingleFrame; + public MenuItem? miIsMenuBorderDisabled; + public MenuItem? miForce16Colors; + public MenuItem? miIsMouseDisabled; - // TableView provides many options for table headers. For simplicity we turn all - // of these off. By enabling FullRowSelect and turning off headers, TableView looks just - // like a ListView - ScenarioList.FullRowSelect = true; - ScenarioList.Style.ShowHeaders = false; - ScenarioList.Style.ShowHorizontalHeaderOverline = false; - ScenarioList.Style.ShowHorizontalHeaderUnderline = false; - ScenarioList.Style.ShowHorizontalBottomline = false; - ScenarioList.Style.ShowVerticalCellLines = false; - ScenarioList.Style.ShowVerticalHeaderLines = false; - - /* By default TableView lays out columns at render time and only - * measures y rows of data at a time. Where y is the height of the - * console. This is for the following reasons: - * - * - Performance, when tables have a large amount of data - * - Defensive, prevents a single wide cell value pushing other - * columns off screen (requiring horizontal scrolling - * - * In the case of UICatalog here, such an approach is overkill so - * we just measure all the data ourselves and set the appropriate - * max widths as ColumnStyles - */ - var longestName = _scenarios!.Max (s => s.GetName ().Length); - ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }); - ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1 }); - - // Enable user to find & select a scenario by typing text - // TableView does not (currently) have built-in CollectionNavigator support (the ability for the - // user to type and the items that match get selected). We implement it in the app instead. - ScenarioList.KeyDown += (s, a) => { - if (CollectionNavigator.IsCompatibleKey (a.KeyEvent)) { - var newItem = _scenarioCollectionNav?.GetNextMatchingItem (ScenarioList.SelectedRow, (char)a.KeyEvent.KeyValue); - if (newItem is int v && newItem != -1) { - ScenarioList.SelectedRow = v; - ScenarioList.EnsureSelectedCellIsVisible (); - ScenarioList.SetNeedsDisplay (); - a.Handled = true; - } + public ListView CategoryList; + + // 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 + // doesn't (currently) have CollectionNavigator support built in, we implement it here, within the app. + public TableView ScenarioList; + CollectionNavigator _scenarioCollectionNav = new (); + + public StatusItem DriverName; + public StatusItem OS; + + public UICatalogTopLevel () + { + _themeMenuItems = CreateThemeMenuItems (); + _themeMenuBarItem = new MenuBarItem ("_Themes", _themeMenuItems); + MenuBar = new MenuBar (new MenuBarItem [] { + new ("_File", new MenuItem [] { + new ("_Quit", "Quit UI Catalog", RequestStop, null, null) + }), + _themeMenuBarItem, + new ("Diag_nostics", CreateDiagnosticMenuItems ()), + new ("_Help", new MenuItem [] { + new ("_Documentation", "", () => OpenUrl ("https://gui-cs.github.io/Terminal.GuiV2Docs"), null, null, (KeyCode)Key.F1), + new ("_README", "", () => OpenUrl ("https://github.com/gui-cs/Terminal.Gui"), null, null, (KeyCode)Key.F2), + new ("_About...", + "About UI Catalog", () => MessageBox.Query ("About UI Catalog", _aboutMessage!.ToString (), 0, false, "_Ok"), null, null, (KeyCode)Key.A.WithCtrl) + }) + }); + + DriverName = new StatusItem (Key.Empty, "Driver:", null); + OS = new StatusItem (Key.Empty, "OS:", null); + + StatusBar = new StatusBar () { + Visible = ShowStatusBar + }; + + StatusBar.Items = new StatusItem [] { + new (Application.QuitKey, $"~{Application.QuitKey} to quit", () => { + if (_selectedScenario is null) { + // This causes GetScenarioToRun to return null + _selectedScenario = null; + RequestStop (); + } else { + _selectedScenario.RequestStop (); } - }; - ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; + }), + new (Key.F10, "~F10~ Status Bar", () => { + StatusBar.Visible = !StatusBar.Visible; + //ContentPane!.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); + LayoutSubviews (); + SetSubViewNeedsDisplay (); + }), + DriverName, + OS + }; + + // Create the Category list view. This list never changes. + CategoryList = new ListView (_categories) { + X = 0, + Y = 1, + Width = Dim.Percent (30), + Height = Dim.Fill (1), + AllowsMarking = false, + CanFocus = true, + Title = "Categories", + BorderStyle = LineStyle.Single, + SuperViewRendersLineCanvas = true + }; + CategoryList.OpenSelectedItem += (s, a) => { + ScenarioList!.SetFocus (); + }; + CategoryList.SelectedItemChanged += CategoryView_SelectedChanged; + + // Create the scenario list. The contents of the scenario list changes whenever the + // Category list selection changes (to show just the scenarios that belong to the selected + // category). + ScenarioList = new TableView () { + X = Pos.Right (CategoryList) - 1, + Y = 1, + Width = Dim.Fill (0), + Height = Dim.Fill (1), + //AllowsMarking = false, + CanFocus = true, + Title = "Scenarios", + BorderStyle = LineStyle.Single, + SuperViewRendersLineCanvas = true + }; + + // TableView provides many options for table headers. For simplicity we turn all + // of these off. By enabling FullRowSelect and turning off headers, TableView looks just + // like a ListView + ScenarioList.FullRowSelect = true; + ScenarioList.Style.ShowHeaders = false; + ScenarioList.Style.ShowHorizontalHeaderOverline = false; + ScenarioList.Style.ShowHorizontalHeaderUnderline = false; + ScenarioList.Style.ShowHorizontalBottomline = false; + ScenarioList.Style.ShowVerticalCellLines = false; + ScenarioList.Style.ShowVerticalHeaderLines = false; + + /* By default TableView lays out columns at render time and only + * measures y rows of data at a time. Where y is the height of the + * console. This is for the following reasons: + * + * - Performance, when tables have a large amount of data + * - Defensive, prevents a single wide cell value pushing other + * columns off screen (requiring horizontal scrolling + * + * In the case of UICatalog here, such an approach is overkill so + * we just measure all the data ourselves and set the appropriate + * max widths as ColumnStyles + */ + int longestName = _scenarios!.Max (s => s.GetName ().Length); + ScenarioList.Style.ColumnStyles.Add (0, new ColumnStyle () { MaxWidth = longestName, MinWidth = longestName, MinAcceptableWidth = longestName }); + ScenarioList.Style.ColumnStyles.Add (1, new ColumnStyle () { MaxWidth = 1 }); + + // Enable user to find & select a scenario by typing text + // TableView does not (currently) have built-in CollectionNavigator support (the ability for the + // user to type and the items that match get selected). We implement it in the app instead. + ScenarioList.KeyDown += (s, a) => { + if (CollectionNavigatorBase.IsCompatibleKey (a)) { + int? newItem = _scenarioCollectionNav?.GetNextMatchingItem (ScenarioList.SelectedRow, (char)a); + if (newItem is int v && newItem != -1) { + ScenarioList.SelectedRow = v; + ScenarioList.EnsureSelectedCellIsVisible (); + ScenarioList.SetNeedsDisplay (); + a.Handled = true; + } + } + }; + ScenarioList.CellActivated += ScenarioView_OpenSelectedItem; + + // TableView typically is a grid where nav keys are biased for moving left/right. + ScenarioList.KeyBindings.Add (Key.Home, Command.TopHome); + ScenarioList.KeyBindings.Add (Key.End, Command.BottomEnd); + + // Ideally, TableView.MultiSelect = false would turn off any keybindings for + // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for + // a shortcut to About. + ScenarioList.MultiSelect = false; + ScenarioList.KeyBindings.Remove (Key.A.WithCtrl); - // TableView typically is a grid where nav keys are biased for moving left/right. - ScenarioList.AddKeyBinding (Key.Home, Command.TopHome); - ScenarioList.AddKeyBinding (Key.End, Command.BottomEnd); + Add (CategoryList); + Add (ScenarioList); - // Ideally, TableView.MultiSelect = false would turn off any keybindings for - // multi-select options. But it currently does not. UI Catalog uses Ctrl-A for - // a shortcut to About. - ScenarioList.MultiSelect = false; - ScenarioList.ClearKeyBinding (Key.CtrlMask | Key.A); + Add (MenuBar); + Add (StatusBar); - KeyDown += KeyDownHandler; + Loaded += LoadedHandler; + Unloaded += UnloadedHandler; - Add (CategoryList); - Add (ScenarioList); + // Restore previous selections + CategoryList.SelectedItem = _cachedCategoryIndex; + ScenarioList.SelectedRow = _cachedScenarioIndex; - Add (MenuBar); - Add (StatusBar); + Applied += ConfigAppliedHandler; + } - Loaded += LoadedHandler; - Unloaded += UnloadedHandler; + void LoadedHandler (object? sender, EventArgs? args) + { + ConfigChanged (); - // Restore previous selections - CategoryList.SelectedItem = _cachedCategoryIndex; - ScenarioList.SelectedRow = _cachedScenarioIndex; + miIsMouseDisabled!.Checked = Application.IsMouseDisabled; + DriverName.Title = $"Driver: {Driver.GetVersionInfo ()}"; + OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}"; - CM.Applied += ConfigAppliedHandler; + if (_selectedScenario != null) { + _selectedScenario = null; + _isFirstRunning = false; + } + if (!_isFirstRunning) { + ScenarioList.SetFocus (); } - void LoadedHandler (object? sender, EventArgs? args) - { - ConfigChanged (); + StatusBar.VisibleChanged += (s, e) => { + ShowStatusBar = StatusBar.Visible; - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; - DriverName.Title = $"Driver: {Driver.GetVersionInfo ()}"; - OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}"; + int height = StatusBar.Visible ? 1 : 0; + CategoryList.Height = Dim.Fill (height); + ScenarioList.Height = Dim.Fill (height); + // ContentPane.Height = Dim.Fill (height); + LayoutSubviews (); + SetSubViewNeedsDisplay (); + }; - if (_selectedScenario != null) { - _selectedScenario = null; - _isFirstRunning = false; - } - if (!_isFirstRunning) { - ScenarioList.SetFocus (); - } + Loaded -= LoadedHandler; + CategoryList.EnsureSelectedItemVisible (); + ScenarioList.EnsureSelectedCellIsVisible (); + } - StatusBar.VisibleChanged += (s, e) => { - UICatalogApp.ShowStatusBar = StatusBar.Visible; + void UnloadedHandler (object? sender, EventArgs? args) + { + Applied -= ConfigAppliedHandler; + Unloaded -= UnloadedHandler; + } - var height = (StatusBar.Visible ? 1 : 0); - CategoryList.Height = Dim.Fill (height); - ScenarioList.Height = Dim.Fill (height); - // ContentPane.Height = Dim.Fill (height); - LayoutSubviews (); - SetSubViewNeedsDisplay (); - }; + void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) + { + ConfigChanged (); + } - Loaded -= LoadedHandler; - CategoryList.EnsureSelectedItemVisible (); - ScenarioList.EnsureSelectedCellIsVisible (); - } + /// + /// Launches the selected scenario, setting the global _selectedScenario + /// + /// + void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) + { + if (_selectedScenario is null) { + // Save selected item state + _cachedCategoryIndex = CategoryList.SelectedItem; + _cachedScenarioIndex = ScenarioList.SelectedRow; - private void UnloadedHandler (object? sender, EventArgs? args) - { - CM.Applied -= ConfigAppliedHandler; - Unloaded -= UnloadedHandler; - } + // Create new instance of scenario (even though Scenarios contains instances) + string selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0]; + _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios!.FirstOrDefault (s => s.GetName () == selectedScenarioName)!.GetType ())!; - void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) - { - ConfigChanged (); + // Tell the main app to stop + Application.RequestStop (); } + } - /// - /// Launches the selected scenario, setting the global _selectedScenario - /// - /// - void ScenarioView_OpenSelectedItem (object? sender, EventArgs? e) - { - if (_selectedScenario is null) { - // Save selected item state - _cachedCategoryIndex = CategoryList.SelectedItem; - _cachedScenarioIndex = ScenarioList.SelectedRow; - - // Create new instance of scenario (even though Scenarios contains instances) - string selectedScenarioName = (string)ScenarioList.Table [ScenarioList.SelectedRow, 0]; - _selectedScenario = (Scenario)Activator.CreateInstance (_scenarios!.FirstOrDefault (s => s.GetName () == selectedScenarioName)!.GetType ())!; + List CreateDiagnosticMenuItems () + { + var menuItems = new List { + CreateDiagnosticFlagsMenuItems (), + new MenuItem [] { null! }, + CreateDisabledEnabledMouseItems (), + CreateDisabledEnabledMenuBorder (), + CreateDisabledEnableUseSubMenusSingleFrame (), + CreateKeyBindingsMenuItems () + }; + return menuItems; + } - // Tell the main app to stop - Application.RequestStop (); - } - } + // TODO: This should be an ConfigurationManager setting + MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame () + { + var menuItems = new List (); + miUseSubMenusSingleFrame = new MenuItem { + Title = "Enable _Sub-Menus Single Frame" + }; + miUseSubMenusSingleFrame.Shortcut = KeyCode.CtrlMask | KeyCode.AltMask | (KeyCode)miUseSubMenusSingleFrame!.Title!.Substring (8, 1) [0]; + miUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; + miUseSubMenusSingleFrame.Action += () => { + miUseSubMenusSingleFrame.Checked = (bool)!miUseSubMenusSingleFrame.Checked!; + MenuBar.UseSubMenusSingleFrame = (bool)miUseSubMenusSingleFrame.Checked; + }; + menuItems.Add (miUseSubMenusSingleFrame); + + return menuItems.ToArray (); + } - List CreateDiagnosticMenuItems () - { - List menuItems = new List { - CreateDiagnosticFlagsMenuItems (), - new MenuItem [] { null! }, - CreateDisabledEnabledMouseItems (), - CreateDisabledEnabledMenuBorder (), - CreateDisabledEnableUseSubMenusSingleFrame (), - CreateKeyBindingsMenuItems () - }; - return menuItems; - } + // TODO: This should be an ConfigurationManager setting + MenuItem [] CreateDisabledEnabledMenuBorder () + { + var menuItems = new List (); + miIsMenuBorderDisabled = new MenuItem { + Title = "Disable Menu _Border" + }; + miIsMenuBorderDisabled.Shortcut = (KeyCode)((Key)miIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]).WithAlt.WithCtrl; + miIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; + miIsMenuBorderDisabled.Action += () => { + miIsMenuBorderDisabled.Checked = (bool)!miIsMenuBorderDisabled.Checked!; + MenuBar.MenusBorderStyle = !(bool)miIsMenuBorderDisabled.Checked ? LineStyle.Single : LineStyle.None; + }; + menuItems.Add (miIsMenuBorderDisabled); + + return menuItems.ToArray (); + } - // TODO: This should be an ConfigurationManager setting - MenuItem [] CreateDisabledEnableUseSubMenusSingleFrame () - { - List menuItems = new List (); - miUseSubMenusSingleFrame = new MenuItem { - Title = "Enable _Sub-Menus Single Frame" - }; - miUseSubMenusSingleFrame.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miUseSubMenusSingleFrame!.Title!.Substring (8, 1) [0]; - miUseSubMenusSingleFrame.CheckType |= MenuItemCheckStyle.Checked; - miUseSubMenusSingleFrame.Action += () => { - miUseSubMenusSingleFrame.Checked = (bool)!miUseSubMenusSingleFrame.Checked!; - MenuBar.UseSubMenusSingleFrame = (bool)miUseSubMenusSingleFrame.Checked; - }; - menuItems.Add (miUseSubMenusSingleFrame); - return menuItems.ToArray (); - } + MenuItem [] CreateForce16ColorItems () + { + var menuItems = new List (); + miForce16Colors = new MenuItem { + Title = "Force _16 Colors", + Shortcut = (KeyCode)Key.F6, + Checked = Application.Force16Colors, + CanExecute = () => (bool)Application.Driver.SupportsTrueColor + }; + miForce16Colors.CheckType |= MenuItemCheckStyle.Checked; + miForce16Colors.Action += () => { + miForce16Colors.Checked = Application.Force16Colors = (bool)!miForce16Colors.Checked!; + Application.Refresh (); + }; + menuItems.Add (miForce16Colors); + + return menuItems.ToArray (); + } - // TODO: This should be an ConfigurationManager setting - MenuItem [] CreateDisabledEnabledMenuBorder () - { - List menuItems = new List (); - miIsMenuBorderDisabled = new MenuItem { - Title = "Disable Menu _Border" - }; - miIsMenuBorderDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMenuBorderDisabled!.Title!.Substring (14, 1) [0]; - miIsMenuBorderDisabled.CheckType |= MenuItemCheckStyle.Checked; - miIsMenuBorderDisabled.Action += () => { - miIsMenuBorderDisabled.Checked = (bool)!miIsMenuBorderDisabled.Checked!; - MenuBar.MenusBorderStyle = !(bool)miIsMenuBorderDisabled.Checked ? LineStyle.Single : LineStyle.None; - }; - menuItems.Add (miIsMenuBorderDisabled); + MenuItem [] CreateDisabledEnabledMouseItems () + { + var menuItems = new List (); + miIsMouseDisabled = new MenuItem { + Title = "_Disable Mouse" + }; + miIsMouseDisabled.Shortcut = (KeyCode)((Key)miIsMouseDisabled!.Title!.Substring (1, 1) [0]).WithAlt.WithCtrl; + miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; + miIsMouseDisabled.Action += () => { + miIsMouseDisabled.Checked = Application.IsMouseDisabled = (bool)!miIsMouseDisabled.Checked!; + }; + menuItems.Add (miIsMouseDisabled); + + return menuItems.ToArray (); + } - return menuItems.ToArray (); - } + MenuItem [] CreateKeyBindingsMenuItems () + { + var menuItems = new List (); + var item = new MenuItem { + Title = "_Key Bindings", + Help = "Change which keys do what" + }; + item.Action += () => { + var dlg = new KeyBindingsDialog (); + Application.Run (dlg); + }; + + menuItems.Add (null!); + menuItems.Add (item); + + return menuItems.ToArray (); + } + MenuItem [] CreateDiagnosticFlagsMenuItems () + { + const string OFF = "Diagnostics: _Off"; + const string FRAME_RULER = "Diagnostics: Frame _Ruler"; + const string FRAME_PADDING = "Diagnostics: _Frame Padding"; + int index = 0; - MenuItem [] CreateForce16ColorItems () - { - List menuItems = new List (); - miForce16Colors = new MenuItem { - Title = "Force 16 _Colors", - Checked = Application.Force16Colors, - CanExecute = () => (bool)Application.Driver.SupportsTrueColor + var menuItems = new List (); + foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { + var item = new MenuItem { + Title = GetDiagnosticsTitle (diag), + Shortcut = (KeyCode)((Key)index.ToString () [0]).WithAlt }; - miForce16Colors.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miForce16Colors!.Title!.Substring (10, 1) [0]; - miForce16Colors.CheckType |= MenuItemCheckStyle.Checked; - miForce16Colors.Action += () => { - miForce16Colors.Checked = Application.Force16Colors = (bool)!miForce16Colors.Checked!; - Application.Refresh (); + index++; + item.CheckType |= MenuItemCheckStyle.Checked; + if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { + item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding + | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; + } else { + item.Checked = _diagnosticFlags.HasFlag (diag); + } + item.Action += () => { + string t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); + if (item.Title == t && item.Checked == false) { + _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); + item.Checked = true; + } else if (item.Title == t && item.Checked == true) { + _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler; + item.Checked = false; + } else { + var f = GetDiagnosticsEnumValue (item.Title); + if (_diagnosticFlags.HasFlag (f)) { + SetDiagnosticsFlag (f, false); + } else { + SetDiagnosticsFlag (f, true); + } + } + foreach (var menuItem in menuItems) { + if (menuItem.Title == t) { + menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) + && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); + } else if (menuItem.Title != t) { + menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); + } + } + ConsoleDriver.Diagnostics = _diagnosticFlags; + Application.Top.SetNeedsDisplay (); }; - menuItems.Add (miForce16Colors); - - return menuItems.ToArray (); + menuItems.Add (item); } + return menuItems.ToArray (); - MenuItem [] CreateDisabledEnabledMouseItems () + string GetDiagnosticsTitle (Enum diag) { - List menuItems = new List (); - miIsMouseDisabled = new MenuItem { - Title = "_Disable Mouse" - }; - miIsMouseDisabled.Shortcut = Key.CtrlMask | Key.AltMask | (Key)miIsMouseDisabled!.Title!.Substring (1, 1) [0]; - miIsMouseDisabled.CheckType |= MenuItemCheckStyle.Checked; - miIsMouseDisabled.Action += () => { - miIsMouseDisabled.Checked = Application.IsMouseDisabled = (bool)!miIsMouseDisabled.Checked!; + return Enum.GetName (_diagnosticFlags.GetType (), diag) switch { + "Off" => OFF, + "FrameRuler" => FRAME_RULER, + "FramePadding" => FRAME_PADDING, + _ => "" }; - menuItems.Add (miIsMouseDisabled); - - return menuItems.ToArray (); } - MenuItem [] CreateKeyBindingsMenuItems () + Enum GetDiagnosticsEnumValue (string title) { - List menuItems = new List (); - var item = new MenuItem { - Title = "_Key Bindings", - Help = "Change which keys do what" + return title switch { + FRAME_RULER => ConsoleDriver.DiagnosticFlags.FrameRuler, + FRAME_PADDING => ConsoleDriver.DiagnosticFlags.FramePadding, + _ => null! }; - item.Action += () => { - var dlg = new KeyBindingsDialog (); - Application.Run (dlg); - }; - - menuItems.Add (null!); - menuItems.Add (item); - - return menuItems.ToArray (); } - MenuItem [] CreateDiagnosticFlagsMenuItems () + void SetDiagnosticsFlag (Enum diag, bool add) { - const string OFF = "Diagnostics: _Off"; - const string FRAME_RULER = "Diagnostics: Frame _Ruler"; - const string FRAME_PADDING = "Diagnostics: _Frame Padding"; - var index = 0; - - List menuItems = new List (); - foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { - var item = new MenuItem { - Title = GetDiagnosticsTitle (diag), - Shortcut = Key.AltMask + index.ToString () [0] - }; - index++; - item.CheckType |= MenuItemCheckStyle.Checked; - if (GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off) == item.Title) { - item.Checked = (_diagnosticFlags & (ConsoleDriver.DiagnosticFlags.FramePadding - | ConsoleDriver.DiagnosticFlags.FrameRuler)) == 0; + switch (diag) { + case ConsoleDriver.DiagnosticFlags.FrameRuler: + if (add) { + _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; } else { - item.Checked = _diagnosticFlags.HasFlag (diag); + _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; } - item.Action += () => { - var t = GetDiagnosticsTitle (ConsoleDriver.DiagnosticFlags.Off); - if (item.Title == t && item.Checked == false) { - _diagnosticFlags &= ~(ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); - item.Checked = true; - } else if (item.Title == t && item.Checked == true) { - _diagnosticFlags |= (ConsoleDriver.DiagnosticFlags.FramePadding | ConsoleDriver.DiagnosticFlags.FrameRuler); - item.Checked = false; - } else { - var f = GetDiagnosticsEnumValue (item.Title); - if (_diagnosticFlags.HasFlag (f)) { - SetDiagnosticsFlag (f, false); - } else { - SetDiagnosticsFlag (f, true); - } - } - foreach (var menuItem in menuItems) { - if (menuItem.Title == t) { - menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) - && !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FramePadding); - } else if (menuItem.Title != t) { - menuItem.Checked = _diagnosticFlags.HasFlag (GetDiagnosticsEnumValue (menuItem.Title)); - } - } - ConsoleDriver.Diagnostics = _diagnosticFlags; - Application.Top.SetNeedsDisplay (); - }; - menuItems.Add (item); - } - return menuItems.ToArray (); - - string GetDiagnosticsTitle (Enum diag) - { - return Enum.GetName (_diagnosticFlags.GetType (), diag) switch { - "Off" => OFF, - "FrameRuler" => FRAME_RULER, - "FramePadding" => FRAME_PADDING, - _ => "", - }; - } - - Enum GetDiagnosticsEnumValue (string title) - { - return title switch { - FRAME_RULER => ConsoleDriver.DiagnosticFlags.FrameRuler, - FRAME_PADDING => ConsoleDriver.DiagnosticFlags.FramePadding, - _ => null!, - }; - } - - void SetDiagnosticsFlag (Enum diag, bool add) - { - switch (diag) { - case ConsoleDriver.DiagnosticFlags.FrameRuler: - if (add) { - _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FrameRuler; - } else { - _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FrameRuler; - } - break; - case ConsoleDriver.DiagnosticFlags.FramePadding: - if (add) { - _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; - } else { - _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; - } - break; - default: - _diagnosticFlags = default; - break; + break; + case ConsoleDriver.DiagnosticFlags.FramePadding: + if (add) { + _diagnosticFlags |= ConsoleDriver.DiagnosticFlags.FramePadding; + } else { + _diagnosticFlags &= ~ConsoleDriver.DiagnosticFlags.FramePadding; } + break; + default: + _diagnosticFlags = default; + break; } } + } - public MenuItem []? CreateThemeMenuItems () - { - var menuItems = CreateForce16ColorItems ().ToList(); - menuItems.Add (null!); - - foreach (var theme in CM.Themes!) { - var item = new MenuItem { - Title = theme.Key, - Shortcut = Key.AltMask + theme.Key [0] - }; - item.CheckType |= MenuItemCheckStyle.Checked; - item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme; - item.Action += () => { - CM.Themes.Theme = _cachedTheme = theme.Key; - CM.Apply (); - }; - menuItems.Add (item); - } + public MenuItem []? CreateThemeMenuItems () + { + var menuItems = CreateForce16ColorItems ().ToList (); + menuItems.Add (null!); - var schemeMenuItems = new List (); - foreach (var sc in Colors.ColorSchemes) { - var item = new MenuItem { - Title = $"_{sc.Key}", - Data = sc.Key, - Shortcut = Key.AltMask | (Key)sc.Key [..1] [0] - }; - item.CheckType |= MenuItemCheckStyle.Radio; - item.Checked = sc.Key == _topLevelColorScheme; - item.Action += () => { - _topLevelColorScheme = (string)item.Data; - foreach (var schemeMenuItem in schemeMenuItems) { - schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; - } - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - Application.Top.SetNeedsDisplay (); - }; - schemeMenuItems.Add (item); - } - menuItems.Add (null!); - var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ()); - menuItems.Add (mbi); + int schemeCount = 0; + foreach (var theme in Themes!) { + var item = new MenuItem { + Title = $"_{theme.Key}", + Shortcut = (KeyCode)((Key)((int)KeyCode.D1 + schemeCount++)).WithCtrl + }; + item.CheckType |= MenuItemCheckStyle.Checked; + item.Checked = theme.Key == _cachedTheme; // CM.Themes.Theme; + item.Action += () => { + Themes.Theme = _cachedTheme = theme.Key; + Apply (); + }; + menuItems.Add (item); + } - return menuItems.ToArray (); + var schemeMenuItems = new List (); + foreach (var sc in Colors.ColorSchemes) { + var item = new MenuItem { + Title = $"_{sc.Key}", + Data = sc.Key + }; + item.CheckType |= MenuItemCheckStyle.Radio; + item.Checked = sc.Key == _topLevelColorScheme; + item.Action += () => { + _topLevelColorScheme = (string)item.Data; + foreach (var schemeMenuItem in schemeMenuItems) { + schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; + } + ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; + Application.Top.SetNeedsDisplay (); + }; + schemeMenuItems.Add (item); } + menuItems.Add (null!); + var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ()); + menuItems.Add (mbi); - public void ConfigChanged () - { - miForce16Colors!.Checked = Application.Force16Colors; + return menuItems.ToArray (); + } - if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { - _topLevelColorScheme = "Base"; - } + public void ConfigChanged () + { + miForce16Colors!.Checked = Application.Force16Colors; - _themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems (); - _themeMenuBarItem!.Children = _themeMenuItems; - foreach (var mi in _themeMenuItems!) { - if (mi is { Parent: null }) { - mi.Parent = _themeMenuBarItem; - } - } + if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { + _topLevelColorScheme = "Base"; + } - var checkedThemeMenu = _themeMenuItems?.Where (m => m?.Checked ?? false).FirstOrDefault (); - if (checkedThemeMenu != null) { - checkedThemeMenu.Checked = false; - } - checkedThemeMenu = _themeMenuItems?.Where (m => m != null && m.Title == CM.Themes?.Theme).FirstOrDefault (); - if (checkedThemeMenu != null) { - CM.Themes!.Theme = checkedThemeMenu.Title!; - checkedThemeMenu.Checked = true; + _themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems (); + _themeMenuBarItem!.Children = _themeMenuItems; + foreach (var mi in _themeMenuItems!) { + if (mi is { Parent: null }) { + mi.Parent = _themeMenuBarItem; } + } - var schemeMenuItems = ((MenuBarItem)_themeMenuItems?.Where (i => i is MenuBarItem)!.FirstOrDefault ()!)!.Children; - foreach (var schemeMenuItem in schemeMenuItems) { - schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; - } + var checkedThemeMenu = _themeMenuItems?.Where (m => m?.Checked ?? false).FirstOrDefault (); + if (checkedThemeMenu != null) { + checkedThemeMenu.Checked = false; + } + checkedThemeMenu = _themeMenuItems?.Where (m => m != null && m.Title == Themes?.Theme).FirstOrDefault (); + if (checkedThemeMenu != null) { + Themes!.Theme = checkedThemeMenu.Title!; + checkedThemeMenu.Checked = true; + } - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; + var schemeMenuItems = ((MenuBarItem)_themeMenuItems?.Where (i => i is MenuBarItem)!.FirstOrDefault ()!)!.Children; + foreach (var schemeMenuItem in schemeMenuItems) { + schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; + } - //ContentPane.LineStyle = FrameView.DefaultBorderStyle; + ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - MenuBar.Menus [0].Children [0].Shortcut = Application.QuitKey; - StatusBar.Items [0].Shortcut = Application.QuitKey; - StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit"; + //ContentPane.LineStyle = FrameView.DefaultBorderStyle; - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; + MenuBar.Menus [0].Children [0].Shortcut = (KeyCode)Application.QuitKey; + StatusBar.Items [0].Shortcut = Application.QuitKey; + StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit"; - var height = (UICatalogApp.ShowStatusBar ? 1 : 0);// + (MenuBar.Visible ? 1 : 0); - //ContentPane.Height = Dim.Fill (height); + miIsMouseDisabled!.Checked = Application.IsMouseDisabled; - StatusBar.Visible = UICatalogApp.ShowStatusBar; + int height = ShowStatusBar ? 1 : 0; // + (MenuBar.Visible ? 1 : 0); + //ContentPane.Height = Dim.Fill (height); - Application.Top.SetNeedsDisplay (); - } + StatusBar.Visible = ShowStatusBar; - void KeyDownHandler (object? sender, KeyEventEventArgs? a) - { - if (a!.KeyEvent.IsCapslock) { - Capslock.Title = "Caps: On"; - StatusBar.SetNeedsDisplay (); - } else { - Capslock.Title = "Caps: Off"; - StatusBar.SetNeedsDisplay (); - } + Application.Top.SetNeedsDisplay (); + } - if (a!.KeyEvent.IsNumlock) { - Numlock.Title = "Num: On"; - StatusBar.SetNeedsDisplay (); - } else { - Numlock.Title = "Num: Off"; - StatusBar.SetNeedsDisplay (); - } + void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) + { + string item = _categories! [e!.Item]; + List newlist; + if (e.Item == 0) { + // First category is "All" + newlist = _scenarios!; + newlist = _scenarios!; - if (a!.KeyEvent.IsScrolllock) { - Scrolllock.Title = "Scroll: On"; - StatusBar.SetNeedsDisplay (); - } else { - Scrolllock.Title = "Scroll: Off"; - StatusBar.SetNeedsDisplay (); - } + } else { + newlist = _scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList (); } - - void CategoryView_SelectedChanged (object? sender, ListViewItemEventArgs? e) - { - var item = _categories! [e!.Item]; - List newlist; - if (e.Item == 0) { - // First category is "All" - newlist = _scenarios!; - newlist = _scenarios!; - - } else { - newlist = _scenarios!.Where (s => s.GetCategories ().Contains (item)).ToList (); - } - ScenarioList.Table = new EnumerableTableSource (newlist, new Dictionary> () { - { "Name", (s) => s.GetName() }, - { "Description", (s) => s.GetDescription() }, - }); - - // Create a collection of just the scenario names (the 1st column in our TableView) - // for CollectionNavigator. - var firstColumnList = new List (); - for (var i = 0; i < ScenarioList.Table.Rows; i++) { - firstColumnList.Add (ScenarioList.Table [i, 0]); - } - _scenarioCollectionNav.Collection = firstColumnList; - + ScenarioList.Table = new EnumerableTableSource (newlist, new Dictionary> () { + { "Name", (s) => s.GetName () }, + { "Description", (s) => s.GetDescription () } + }); + + // Create a collection of just the scenario names (the 1st column in our TableView) + // for CollectionNavigator. + var firstColumnList = new List (); + for (int i = 0; i < ScenarioList.Table.Rows; i++) { + firstColumnList.Add (ScenarioList.Table [i, 0]); } + _scenarioCollectionNav.Collection = firstColumnList; + } + } - static void VerifyObjectsWereDisposed () - { + static void VerifyObjectsWereDisposed () + { #if DEBUG_IDISPOSABLE - // Validate there are no outstanding Responder-based instances - // after a scenario was selected to run. This proves the main UI Catalog - // 'app' closed cleanly. - foreach (var inst in Responder.Instances) { + // Validate there are no outstanding Responder-based instances + // after a scenario was selected to run. This proves the main UI Catalog + // 'app' closed cleanly. + foreach (var inst in Responder.Instances) { - Debug.Assert (inst.WasDisposed); - } - Responder.Instances.Clear (); + Debug.Assert (inst.WasDisposed); + } + Responder.Instances.Clear (); - // Validate there are no outstanding Application.RunState-based instances - // after a scenario was selected to run. This proves the main UI Catalog - // 'app' closed cleanly. - foreach (var inst in RunState.Instances) { - Debug.Assert (inst.WasDisposed); - } - RunState.Instances.Clear (); -#endif + // Validate there are no outstanding Application.RunState-based instances + // after a scenario was selected to run. This proves the main UI Catalog + // 'app' closed cleanly. + foreach (var inst in RunState.Instances) { + Debug.Assert (inst.WasDisposed); } + RunState.Instances.Clear (); +#endif + } - static void OpenUrl (string url) - { - try { - if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { - url = url.Replace ("&", "^&"); - Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); - } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { - using var process = new Process { - StartInfo = new ProcessStartInfo { - FileName = "xdg-open", - Arguments = url, - RedirectStandardError = true, - RedirectStandardOutput = true, - CreateNoWindow = true, - UseShellExecute = false - } - }; - process.Start (); - } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { - Process.Start ("open", url); - } - } catch { - throw; + static void OpenUrl (string url) + { + try { + if (RuntimeInformation.IsOSPlatform (OSPlatform.Windows)) { + url = url.Replace ("&", "^&"); + Process.Start (new ProcessStartInfo ("cmd", $"/c start {url}") { CreateNoWindow = true }); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.Linux)) { + using var process = new Process { + StartInfo = new ProcessStartInfo { + FileName = "xdg-open", + Arguments = url, + RedirectStandardError = true, + RedirectStandardOutput = true, + CreateNoWindow = true, + UseShellExecute = false + } + }; + process.Start (); + } else if (RuntimeInformation.IsOSPlatform (OSPlatform.OSX)) { + Process.Start ("open", url); } + } catch { + throw; } } -} +} \ No newline at end of file diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 5e2393d2e8..d0a9e63bb3 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -1,8 +1,7 @@  Exe - net7.0 - 10.0 + net8.0 UICatalog.UICatalogApp @@ -30,7 +29,7 @@ - + diff --git a/UnitTests/Application/ApplicationTests.cs b/UnitTests/Application/ApplicationTests.cs index 2a54dadcfe..3c101a0bba 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -13,11 +13,11 @@ namespace Terminal.Gui.ApplicationTests; public class ApplicationTests { - readonly ITestOutputHelper output; + readonly ITestOutputHelper _output; public ApplicationTests (ITestOutputHelper output) { - this.output = output; + this._output = output; #if DEBUG_IDISPOSABLE Responder.Instances.Clear (); RunState.Instances.Clear (); @@ -465,7 +465,7 @@ public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving () │ │ │ │ │ │ - └───┘", output); + └───┘", _output); var attributes = new Attribute [] { // 0 @@ -498,7 +498,7 @@ public void Run_A_Modal_Toplevel_Refresh_Background_On_Moving () │ │ │ │ │ │ - └───┘", output); + └───┘", _output); attributes = new Attribute [] { // 0 @@ -654,316 +654,11 @@ public void Internal_Properties_Correct () Assert.NotNull (Application.Top); var rs = Application.Begin (Application.Top); Assert.Equal (Application.Top, rs.Toplevel); - Assert.Null (Application.MouseGrabView); // public + Assert.Null (Application.MouseGrabView); // public Assert.Null (Application.WantContinuousButtonPressedView); // public Assert.False (Application.MoveToOverlappedChild (Application.Top)); } - #region KeyboardTests - [Fact] - public void KeyUp_Event () - { - // Setup Mock driver - Init (); - - // Setup some fake keypresses (This) - var input = "Tests"; - - // Put a control-q in at the end - FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('q', ConsoleKey.Q, shift: false, alt: false, control: true)); - foreach (var c in input.Reverse ()) { - if (char.IsLetter (c)) { - FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (c, (ConsoleKey)char.ToUpper (c), shift: char.IsUpper (c), alt: false, control: false)); - } else { - FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (c, (ConsoleKey)c, shift: false, alt: false, control: false)); - } - } - - int stackSize = FakeConsole.MockKeyPresses.Count; - - int iterations = 0; - Application.Iteration += (s, a) => { - iterations++; - // Stop if we run out of control... - if (iterations > 10) { - Application.RequestStop (); - } - }; - - int keyUps = 0; - var output = string.Empty; - Application.Top.KeyUp += (object sender, KeyEventEventArgs args) => { - if (args.KeyEvent.Key != (Key.CtrlMask | Key.Q)) { - output += (char)args.KeyEvent.KeyValue; - } - keyUps++; - }; - - Application.Run (Application.Top); - - // Input string should match output - Assert.Equal (input, output); - - // # of key up events should match stack size - //Assert.Equal (stackSize, keyUps); - // We can't use numbers variables on the left side of an Assert.Equal/NotEqual, - // it must be literal (Linux only). - Assert.Equal (6, keyUps); - - // # of key up events should match # of iterations - Assert.Equal (stackSize, iterations); - - Application.Shutdown (); - Assert.Null (Application.Current); - Assert.Null (Application.Top); - Assert.Null (Application.MainLoop); - Assert.Null (Application.Driver); - } - - [Fact] - public void AlternateForwardKey_AlternateBackwardKey_Tests () - { - Init (); - - var top = Application.Top; - var w1 = new Window (); - var v1 = new TextField (); - var v2 = new TextView (); - w1.Add (v1, v2); - - var w2 = new Window (); - var v3 = new CheckBox (); - var v4 = new Button (); - w2.Add (v3, v4); - - top.Add (w1, w2); - - Application.Iteration += (s, a) => { - Assert.True (v1.HasFocus); - // Using default keys. - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, - new KeyModifiers () { Ctrl = true })); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, - new KeyModifiers () { Ctrl = true })); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, - new KeyModifiers () { Ctrl = true })); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, - new KeyModifiers () { Ctrl = true })); - Assert.True (v1.HasFocus); - - top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab, - new KeyModifiers () { Shift = true, Ctrl = true })); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab, - new KeyModifiers () { Shift = true, Ctrl = true })); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab, - new KeyModifiers () { Shift = true, Ctrl = true })); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.ShiftMask | Key.CtrlMask | Key.Tab, - new KeyModifiers () { Shift = true, Ctrl = true })); - Assert.True (v1.HasFocus); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown, - new KeyModifiers () { Ctrl = true })); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown, - new KeyModifiers () { Ctrl = true })); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown, - new KeyModifiers () { Ctrl = true })); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageDown, - new KeyModifiers () { Ctrl = true })); - Assert.True (v1.HasFocus); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp, - new KeyModifiers () { Ctrl = true })); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp, - new KeyModifiers () { Ctrl = true })); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp, - new KeyModifiers () { Ctrl = true })); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.PageUp, - new KeyModifiers () { Ctrl = true })); - Assert.True (v1.HasFocus); - - // Using another's alternate keys. - Application.AlternateForwardKey = Key.F7; - Application.AlternateBackwardKey = Key.F6; - - top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ())); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ())); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ())); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.F7, new KeyModifiers ())); - Assert.True (v1.HasFocus); - - top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ())); - Assert.True (v4.HasFocus); - top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ())); - Assert.True (v3.HasFocus); - top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ())); - Assert.True (v2.HasFocus); - top.ProcessKey (new KeyEvent (Key.F6, new KeyModifiers ())); - Assert.True (v1.HasFocus); - - Application.RequestStop (); - }; - - Application.Run (top); - - // Replacing the defaults keys to avoid errors on others unit tests that are using it. - Application.AlternateForwardKey = Key.PageDown | Key.CtrlMask; - Application.AlternateBackwardKey = Key.PageUp | Key.CtrlMask; - Application.QuitKey = Key.Q | Key.CtrlMask; - - Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey); - Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey); - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); - } - - [Fact] - [AutoInitShutdown] - public void QuitKey_Getter_Setter () - { - var top = Application.Top; - var isQuiting = false; - - top.Closing += (s, e) => { - isQuiting = true; - e.Cancel = true; - }; - - Application.Begin (top); - top.Running = true; - - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); - Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); - Assert.True (isQuiting); - - isQuiting = false; - Application.QuitKey = Key.C | Key.CtrlMask; - - Application.Driver.SendKeys ('q', ConsoleKey.Q, false, false, true); - Assert.False (isQuiting); - Application.Driver.SendKeys ('c', ConsoleKey.C, false, false, true); - Assert.True (isQuiting); - - // Reset the QuitKey to avoid throws errors on another tests - Application.QuitKey = Key.Q | Key.CtrlMask; - } - - [Fact] - [AutoInitShutdown] - public void EnsuresTopOnFront_CanFocus_True_By_Keyboard_And_Mouse () - { - var top = Application.Top; - var win = new Window () { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; - var tf = new TextField () { Width = 10 }; - win.Add (tf); - var win2 = new Window () { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; - var tf2 = new TextField () { Width = 10 }; - win2.Add (tf2); - top.Add (win, win2); - - Application.Begin (top); - - Assert.True (win.CanFocus); - Assert.True (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); - Assert.True (win.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); - Assert.True (win.CanFocus); - Assert.True (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.False (win2.HasFocus); - Assert.Equal ("win", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); - Assert.True (win.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); - Assert.Null (Toplevel._dragPosition); - } - - [Fact] - [AutoInitShutdown] - public void EnsuresTopOnFront_CanFocus_False_By_Keyboard_And_Mouse () - { - var top = Application.Top; - var win = new Window () { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; - var tf = new TextField () { Width = 10 }; - win.Add (tf); - var win2 = new Window () { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; - var tf2 = new TextField () { Width = 10 }; - win2.Add (tf2); - top.Add (win, win2); - - Application.Begin (top); - - Assert.True (win.CanFocus); - Assert.True (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.False (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - win.CanFocus = false; - Assert.False (win.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); - Assert.True (win2.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - top.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Tab, new KeyModifiers ())); - Assert.False (win.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - - win.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); - Assert.False (win.CanFocus); - Assert.False (win.HasFocus); - Assert.True (win2.CanFocus); - Assert.True (win2.HasFocus); - Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); - win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); - Assert.Null (Toplevel._dragPosition); - } - - #endregion - - // Invoke Tests // TODO: Test with threading scenarios [Fact] @@ -976,6 +671,7 @@ public void Invoke_Adds_Idle () var actionCalled = 0; Application.Invoke (() => { actionCalled++; }); + Application.MainLoop.Running = true; Application.RunIteration (ref rs, ref firstIteration); Assert.Equal (1, actionCalled); Application.Shutdown (); diff --git a/UnitTests/Application/KeyboardTests.cs b/UnitTests/Application/KeyboardTests.cs new file mode 100644 index 0000000000..97facc89c9 --- /dev/null +++ b/UnitTests/Application/KeyboardTests.cs @@ -0,0 +1,396 @@ +using System; +using System.Linq; +using Xunit; +using Xunit.Abstractions; + +namespace Terminal.Gui.ApplicationTests; + +public class KeyboardTests { + readonly ITestOutputHelper _output; + + public KeyboardTests (ITestOutputHelper output) + { + this._output = output; +#if DEBUG_IDISPOSABLE + Responder.Instances.Clear (); + RunState.Instances.Clear (); +#endif + } + + [Fact] + public void KeyUp_Event () + { + Application.Init (new FakeDriver ()); + + // Setup some fake keypresses (This) + var input = "Tests"; + + // Put a control-q in at the end + FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo ('Q', ConsoleKey.Q, shift: false, alt: false, control: true)); + foreach (var c in input.Reverse ()) { + if (char.IsLetter (c)) { + FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (c, (ConsoleKey)char.ToUpper (c), shift: char.IsUpper (c), alt: false, control: false)); + } else { + FakeConsole.MockKeyPresses.Push (new ConsoleKeyInfo (c, (ConsoleKey)c, shift: false, alt: false, control: false)); + } + } + + int stackSize = FakeConsole.MockKeyPresses.Count; + + int iterations = 0; + Application.Iteration += (s, a) => { + iterations++; + // Stop if we run out of control... + if (iterations > 10) { + Application.RequestStop (); + } + }; + + int keyUps = 0; + var output = string.Empty; + Application.Top.KeyUp += (object sender, Key args) => { + if (args.KeyCode != (KeyCode.CtrlMask | KeyCode.Q)) { + output += args.AsRune; + } + keyUps++; + }; + + Application.Run (Application.Top); + + // Input string should match output + Assert.Equal (input, output); + + // # of key up events should match stack size + //Assert.Equal (stackSize, keyUps); + // We can't use numbers variables on the left side of an Assert.Equal/NotEqual, + // it must be literal (Linux only). + Assert.Equal (6, keyUps); + + // # of key up events should match # of iterations + Assert.Equal (stackSize, iterations); + + Application.Shutdown (); + Assert.Null (Application.Current); + Assert.Null (Application.Top); + Assert.Null (Application.MainLoop); + Assert.Null (Application.Driver); + } + + [Fact] + public void AlternateForwardKey_AlternateBackwardKey_Tests () + { + Application.Init (new FakeDriver ()); + + var top = Application.Top; + var w1 = new Window (); + var v1 = new TextField (); + var v2 = new TextView (); + w1.Add (v1, v2); + + var w2 = new Window (); + var v3 = new CheckBox (); + var v4 = new Button (); + w2.Add (v3, v4); + + top.Add (w1, w2); + + Application.Iteration += (s, a) => { + Assert.True (v1.HasFocus); + // Using default keys. + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v1.HasFocus); + + top.NewKeyDownEvent (new (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (v1.HasFocus); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageDown)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageDown)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageDown)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageDown)); + Assert.True (v1.HasFocus); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageUp)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageUp)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageUp)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.PageUp)); + Assert.True (v1.HasFocus); + + // Using another's alternate keys. + Application.AlternateForwardKey = KeyCode.F7; + Application.AlternateBackwardKey = KeyCode.F6; + + top.NewKeyDownEvent (new (KeyCode.F7)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F7)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F7)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F7)); + Assert.True (v1.HasFocus); + + top.NewKeyDownEvent (new (KeyCode.F6)); + Assert.True (v4.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F6)); + Assert.True (v3.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F6)); + Assert.True (v2.HasFocus); + top.NewKeyDownEvent (new (KeyCode.F6)); + Assert.True (v1.HasFocus); + + Application.RequestStop (); + }; + + Application.Run (top); + + // Replacing the defaults keys to avoid errors on others unit tests that are using it. + Application.AlternateForwardKey = KeyCode.PageDown | KeyCode.CtrlMask; + Application.AlternateBackwardKey = KeyCode.PageUp | KeyCode.CtrlMask; + Application.QuitKey = KeyCode.Q | KeyCode.CtrlMask; + + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey.KeyCode); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + + // Shutdown must be called to safely clean up Application if Init has been called + Application.Shutdown (); + } + + [Fact] + [AutoInitShutdown] + public void QuitKey_Getter_Setter () + { + var top = Application.Top; + var isQuiting = false; + + top.Closing += (s, e) => { + isQuiting = true; + e.Cancel = true; + }; + + Application.Begin (top); + top.Running = true; + + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); + Assert.True (isQuiting); + + isQuiting = false; + Application.OnKeyDown(new Key ( KeyCode.Q | KeyCode.CtrlMask)); + Assert.True (isQuiting); + + isQuiting = false; + Application.QuitKey = KeyCode.C | KeyCode.CtrlMask; + Application.Driver.SendKeys ('Q', ConsoleKey.Q, false, false, true); + Assert.False (isQuiting); + Application.OnKeyDown (new Key (KeyCode.Q | KeyCode.CtrlMask)); + Assert.False (isQuiting); + + Application.OnKeyDown (Application.QuitKey); + Assert.True (isQuiting); + + // Reset the QuitKey to avoid throws errors on another tests + Application.QuitKey = KeyCode.Q | KeyCode.CtrlMask; + } + + [Fact] + [AutoInitShutdown] + public void EnsuresTopOnFront_CanFocus_True_By_Keyboard_And_Mouse () + { + var top = Application.Top; + var win = new Window () { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; + var tf = new TextField () { Width = 10 }; + win.Add (tf); + var win2 = new Window () { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; + var tf2 = new TextField () { Width = 10 }; + win2.Add (tf2); + top.Add (win, win2); + + Application.Begin (top); + + Assert.True (win.CanFocus); + Assert.True (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.False (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (win.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (win.CanFocus); + Assert.True (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.False (win2.HasFocus); + Assert.Equal ("win", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); + Assert.True (win.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); + Assert.Null (Toplevel._dragPosition); + } + + [Fact] + [AutoInitShutdown] + public void EnsuresTopOnFront_CanFocus_False_By_Keyboard_And_Mouse () + { + var top = Application.Top; + var win = new Window () { Title = "win", X = 0, Y = 0, Width = 20, Height = 10 }; + var tf = new TextField () { Width = 10 }; + win.Add (tf); + var win2 = new Window () { Title = "win2", X = 22, Y = 0, Width = 20, Height = 10 }; + var tf2 = new TextField () { Width = 10 }; + win2.Add (tf2); + top.Add (win, win2); + + Application.Begin (top); + + Assert.True (win.CanFocus); + Assert.True (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.False (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + win.CanFocus = false; + Assert.False (win.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.True (win2.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + top.NewKeyDownEvent (new (KeyCode.CtrlMask | KeyCode.Tab)); + Assert.False (win.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + + win.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Pressed }); + Assert.False (win.CanFocus); + Assert.False (win.HasFocus); + Assert.True (win2.CanFocus); + Assert.True (win2.HasFocus); + Assert.Equal ("win2", ((Window)top.Subviews [top.Subviews.Count - 1]).Title); + win2.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Released }); + Assert.Null (Toplevel._dragPosition); + } + + // test Application key Bindings + public class ScopedKeyBindingView : View { + public bool ApplicationCommand { get; set; } + public bool HotKeyCommand { get; set; } + public bool FocusedCommand { get; set; } + + public ScopedKeyBindingView () + { + AddCommand (Command.Save, () => ApplicationCommand = true); + AddCommand (Command.Default, () => HotKeyCommand = true); + AddCommand (Command.Left, () => FocusedCommand = true); + + KeyBindings.Add (KeyCode.A, KeyBindingScope.Application, Command.Save); + HotKey = KeyCode.H; + KeyBindings.Add (KeyCode.F, KeyBindingScope.Focused, Command.Left); + } + } + + [Fact] + [AutoInitShutdown] + public void OnKeyDown_Application_KeyBinding () + { + var view = new ScopedKeyBindingView (); + var invoked = false; + view.InvokingKeyBindings += (s, e) => invoked = true; + + Application.Top.Add (view); + Application.Begin (Application.Top); + + Application.OnKeyDown (new (KeyCode.A)); + Assert.True (invoked); + Assert.True (view.ApplicationCommand); + + invoked = false; + view.ApplicationCommand = false; + view.KeyBindings.Remove (KeyCode.A); + Application.OnKeyDown (new (KeyCode.A)); // old + Assert.False (invoked); + Assert.False (view.ApplicationCommand); + view.KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, KeyBindingScope.Application, Command.Save); + Application.OnKeyDown (new (KeyCode.A)); // old + Assert.False (invoked); + Assert.False (view.ApplicationCommand); + Application.OnKeyDown (new (KeyCode.A | KeyCode.CtrlMask)); // new + Assert.True (invoked); + Assert.True (view.ApplicationCommand); + + invoked = false; + Application.OnKeyDown (new (KeyCode.H)); + Assert.True (invoked); + + invoked = false; + Assert.False (view.HasFocus); + Application.OnKeyDown (new (KeyCode.F)); + Assert.False (invoked); + + Assert.True (view.ApplicationCommand); + Assert.True (view.HotKeyCommand); + Assert.False (view.FocusedCommand); + } + + [Fact] + [AutoInitShutdown] + public void OnKeyDown_Application_KeyBinding_Negative () + { + var view = new ScopedKeyBindingView (); + var invoked = false; + view.InvokingKeyBindings += (s, e) => invoked = true; + + Application.Top.Add (view); + Application.Begin (Application.Top); + + Application.OnKeyDown (new (KeyCode.A | KeyCode.CtrlMask)); + Assert.False (invoked); + Assert.False (view.ApplicationCommand); + Assert.False (view.HotKeyCommand); + Assert.False (view.FocusedCommand); + + invoked = false; + Assert.False (view.HasFocus); + Application.OnKeyDown (new (KeyCode.Z)); + Assert.False (invoked); + Assert.False (view.ApplicationCommand); + Assert.False (view.HotKeyCommand); + Assert.False (view.FocusedCommand); + } +} \ No newline at end of file diff --git a/UnitTests/Application/MainLoopTests.cs b/UnitTests/Application/MainLoopTests.cs index e8bf7b91c3..d8bd2d1a27 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -627,8 +627,8 @@ public async Task InvokeLeakTest () Application.Top.Add (tf); const int numPasses = 5; - const int numIncrements = 5000; - const int pollMs = 10000; + const int numIncrements = 500; + const int pollMs = 2500; var task = Task.Run (() => RunTest (r, tf, numPasses, numIncrements, pollMs)); @@ -681,7 +681,7 @@ public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions (Action if (iterations == 0) { Assert.Null (btn); Assert.Equal (zero, total); - Assert.True (btnLaunch.ProcessKey (new KeyEvent (Key.Enter, null))); + Assert.True (btnLaunch.NewKeyDownEvent (new (KeyCode.Space))); if (btn == null) { Assert.Null (btn); Assert.Equal (zero, total); @@ -692,7 +692,7 @@ public void Mainloop_Invoke_Or_AddIdle_Can_Be_Used_For_Events_Or_Actions (Action } else if (iterations == 1) { Assert.Equal (clickMe, btn.Text); Assert.Equal (zero, total); - Assert.True (btn.ProcessKey (new KeyEvent (Key.Enter, null))); + Assert.True (btn.NewKeyDownEvent (new (KeyCode.Space))); Assert.Equal (cancel, btn.Text); Assert.Equal (one, total); } else if (taskCompleted) { diff --git a/UnitTests/Clipboard/ClipboardTests.cs b/UnitTests/Clipboard/ClipboardTests.cs index 1440780f92..9c1f33d692 100644 --- a/UnitTests/Clipboard/ClipboardTests.cs +++ b/UnitTests/Clipboard/ClipboardTests.cs @@ -2,141 +2,152 @@ using Xunit; using Xunit.Abstractions; -namespace Terminal.Gui.ClipboardTests { - public class ClipboardTests { - readonly ITestOutputHelper output; +namespace Terminal.Gui.ClipboardTests; - public ClipboardTests (ITestOutputHelper output) - { - this.output = output; - } - - [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] - public void IClipboard_GetClipBoardData_Throws_NotSupportedException () - { - IClipboard iclip = Application.Driver.Clipboard; - Assert.Throws (() => iclip.GetClipboardData ()); - } +#if RUN_CLIPBOARD_UNIT_TESTS +public class ClipboardTests { + readonly ITestOutputHelper output; - [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] - public void IClipboard_SetClipBoardData_Throws_NotSupportedException () - { - IClipboard iclip = Application.Driver.Clipboard; - Assert.Throws (() => iclip.SetClipboardData ("foo")); - } - - [Fact, AutoInitShutdown (useFakeClipboard: true)] - public void Contents_Fake_Gets_Sets () - { - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } + public ClipboardTests (ITestOutputHelper output) + { + this.output = output; + } - var clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; - Clipboard.Contents = clipText; + [Fact] [AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] + public void IClipboard_GetClipBoardData_Throws_NotSupportedException () + { + var iclip = Application.Driver.Clipboard; + Assert.Throws (() => iclip.GetClipboardData ()); + } - Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + [Fact] [AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] + public void IClipboard_SetClipBoardData_Throws_NotSupportedException () + { + var iclip = Application.Driver.Clipboard; + Assert.Throws (() => iclip.SetClipboardData ("foo")); + } - Assert.Equal (clipText, Clipboard.Contents); + [Fact] [AutoInitShutdown (useFakeClipboard: true)] + public void Contents_Fake_Gets_Sets () + { + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void Contents_Gets_Sets () - { - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } + string clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; + Clipboard.Contents = clipText; - var clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; - Clipboard.Contents = clipText; + Application.Iteration += (s, a) => Application.RequestStop (); + Application.Run (); - Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + Assert.Equal (clipText, Clipboard.Contents); + } - Assert.Equal (clipText, Clipboard.Contents); + [Fact] [AutoInitShutdown (useFakeClipboard: false)] + public void Contents_Gets_Sets () + { + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void Contents_Gets_Sets_When_IsSupportedFalse () - { + string clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; + Clipboard.Contents = clipText; - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } + Application.Iteration += (s, a) => Application.RequestStop (); + Application.Run (); - var clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; - Clipboard.Contents = clipText; + Assert.Equal (clipText, Clipboard.Contents); + } - Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + [Fact] [AutoInitShutdown (useFakeClipboard: false)] + public void Contents_Gets_Sets_When_IsSupportedFalse () + { - Assert.Equal (clipText, Clipboard.Contents); + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: true)] - public void Contents_Fake_Gets_Sets_When_IsSupportedFalse () - { + string clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; + Clipboard.Contents = clipText; - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } + Application.Iteration += (s, a) => Application.RequestStop (); + Application.Run (); - var clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; - Clipboard.Contents = clipText; + Assert.Equal (clipText, Clipboard.Contents); + } - Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + [Fact] [AutoInitShutdown (useFakeClipboard: true)] + public void Contents_Fake_Gets_Sets_When_IsSupportedFalse () + { - Assert.Equal (clipText, Clipboard.Contents); + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void IsSupported_Get () - { - if (Clipboard.IsSupported) Assert.True (Clipboard.IsSupported); - else Assert.False (Clipboard.IsSupported); + string clipText = "The Contents_Gets_Sets unit test pasted this to the OS clipboard."; + Clipboard.Contents = clipText; + + Application.Iteration += (s, a) => Application.RequestStop (); + Application.Run (); + + Assert.Equal (clipText, Clipboard.Contents); + } + + [Fact] [AutoInitShutdown (useFakeClipboard: false)] + public void IsSupported_Get () + { + if (Clipboard.IsSupported) { + Assert.True (Clipboard.IsSupported); + } else { + Assert.False (Clipboard.IsSupported); } + } - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void TryGetClipboardData_Gets_From_OS_Clipboard () - { - var clipText = "The TryGetClipboardData_Gets_From_OS_Clipboard unit test pasted this to the OS clipboard."; - Clipboard.Contents = clipText; + [Fact] [AutoInitShutdown (useFakeClipboard: false)] + public void TryGetClipboardData_Gets_From_OS_Clipboard () + { + string clipText = "The TryGetClipboardData_Gets_From_OS_Clipboard unit test pasted this to the OS clipboard."; + Clipboard.Contents = clipText; - Application.Iteration += (s, a) => Application.RequestStop (); + Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + Application.Run (); - if (Clipboard.IsSupported) { - Assert.True (Clipboard.TryGetClipboardData (out string result)); - Assert.Equal (clipText, result); - } else { - Assert.False (Clipboard.TryGetClipboardData (out string result)); - Assert.NotEqual (clipText, result); - } + if (Clipboard.IsSupported) { + Assert.True (Clipboard.TryGetClipboardData (out string result)); + Assert.Equal (clipText, result); + } else { + Assert.False (Clipboard.TryGetClipboardData (out string result)); + Assert.NotEqual (clipText, result); } + } - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void TrySetClipboardData_Sets_The_OS_Clipboard () - { - var clipText = "The TrySetClipboardData_Sets_The_OS_Clipboard unit test pasted this to the OS clipboard."; - if (Clipboard.IsSupported) Assert.True (Clipboard.TrySetClipboardData (clipText)); - else Assert.False (Clipboard.TrySetClipboardData (clipText)); + [Fact] [AutoInitShutdown (useFakeClipboard: false)] + public void TrySetClipboardData_Sets_The_OS_Clipboard () + { + string clipText = "The TrySetClipboardData_Sets_The_OS_Clipboard unit test pasted this to the OS clipboard."; + if (Clipboard.IsSupported) { + Assert.True (Clipboard.TrySetClipboardData (clipText)); + } else { + Assert.False (Clipboard.TrySetClipboardData (clipText)); + } - Application.Iteration += (s, a) => Application.RequestStop (); + Application.Iteration += (s, a) => Application.RequestStop (); - Application.Run (); + Application.Run (); - if (Clipboard.IsSupported) Assert.Equal (clipText, Clipboard.Contents); - else Assert.NotEqual (clipText, Clipboard.Contents); + if (Clipboard.IsSupported) { + Assert.Equal (clipText, Clipboard.Contents); + } else { + Assert.NotEqual (clipText, Clipboard.Contents); } + } - // Disabling this test for now because it is not reliable + // Disabling this test for now because it is not reliable #if false [Fact, AutoInitShutdown (useFakeClipboard: false)] public void Contents_Copies_From_OS_Clipboard () @@ -267,21 +278,20 @@ public void Contents_Pastes_To_OS_Clipboard () } #endif - bool Is_WSL_Platform () - { - var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"uname -a\""); - return result.Contains ("microsoft") && result.Contains ("WSL"); - } + bool Is_WSL_Platform () + { + (int _, string result) = ClipboardProcessRunner.Process ("bash", $"-c \"uname -a\""); + return result.Contains ("microsoft") && result.Contains ("WSL"); + } - bool xclipExists () - { - try { - var (_, result) = ClipboardProcessRunner.Process ("bash", $"-c \"which xclip\""); - return result.TrimEnd () != ""; - } catch (Exception) { - return false; - } + bool xclipExists () + { + try { + (int _, string result) = ClipboardProcessRunner.Process ("bash", $"-c \"which xclip\""); + return result.TrimEnd () != ""; + } catch (Exception) { + return false; } - } } +#endif diff --git a/UnitTests/Configuration/ConfigurationMangerTests.cs b/UnitTests/Configuration/ConfigurationMangerTests.cs index 5bd36a38cd..70b2adadb3 100644 --- a/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -217,16 +217,16 @@ public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () ConfigurationManager.Locations = ConfigLocations.DefaultOnly; // arrange ConfigurationManager.Reset (); - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q; - ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F; - ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.Q); + ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Settings.Apply (); // assert apply worked - Assert.Equal (Key.Q, Application.QuitKey); - Assert.Equal (Key.F, Application.AlternateForwardKey); - Assert.Equal (Key.B, Application.AlternateBackwardKey); + Assert.Equal (KeyCode.Q, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.F, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.B, Application.AlternateBackwardKey.KeyCode); Assert.True (Application.IsMouseDisabled); //act @@ -235,15 +235,15 @@ public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () // assert Assert.NotEmpty (ConfigurationManager.Themes); Assert.Equal ("Default", ConfigurationManager.Themes.Theme); - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); - Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey); - Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey.KeyCode); Assert.False (Application.IsMouseDisabled); // arrange - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q; - ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F; - ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.Q); + ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Settings.Apply (); @@ -256,9 +256,9 @@ public void Reset_and_ResetLoadWithLibraryResourcesOnly_are_same () // assert Assert.NotEmpty (ConfigurationManager.Themes); Assert.Equal ("Default", ConfigurationManager.Themes.Theme); - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); - Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey); - Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, Application.AlternateBackwardKey.KeyCode); Assert.False (Application.IsMouseDisabled); } @@ -320,7 +320,7 @@ public void TestConfigurationManagerInitDriver () Assert.Equal ("Default", ConfigurationManager.Themes.Theme); Assert.True (ConfigurationManager.Themes.ContainsKey ("Default")); - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"].Normal.Foreground); Assert.Equal (new Color (Color.Blue), Colors.ColorSchemes ["Base"].Normal.Background); @@ -358,10 +358,7 @@ public void TestConfigurationManagerUpdateFromJson () { ""$schema"": ""https://gui-cs.github.io/Terminal.Gui/schemas/tui-config-schema.json"", ""Application.QuitKey"": { - ""Key"": ""Z"", - ""Modifiers"": [ - ""Alt"" - ] + ""Key"": ""Alt-Z"" }, ""Theme"": ""Default"", ""Themes"": [ @@ -500,9 +497,9 @@ public void TestConfigurationManagerUpdateFromJson () ConfigurationManager.ThrowOnJsonErrors = true; ConfigurationManager.Settings.Update (json, "TestConfigurationManagerUpdateFromJson"); - - Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); - Assert.Equal (Key.Z | Key.AltMask, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue); + + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.Z | KeyCode.AltMask, ((Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue).KeyCode); Assert.Equal ("Default", ConfigurationManager.Themes.Theme); @@ -516,7 +513,7 @@ public void TestConfigurationManagerUpdateFromJson () // Now re-apply ConfigurationManager.Apply (); - Assert.Equal (Key.Z | Key.AltMask, Application.QuitKey); + Assert.Equal (KeyCode.Z | KeyCode.AltMask, Application.QuitKey.KeyCode); Assert.Equal ("Default", ConfigurationManager.Themes.Theme); Assert.Equal (new Color (Color.White), Colors.ColorSchemes ["Base"].Normal.Foreground); @@ -735,9 +732,9 @@ public void Load_FiresUpdated () { ConfigurationManager.Reset (); - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q; - ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F; - ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.Q); + ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Updated += ConfigurationManager_Updated; @@ -746,9 +743,9 @@ void ConfigurationManager_Updated (object sender, ConfigurationManagerEventArgs { fired = true; // assert - Assert.Equal (Key.Q | Key.CtrlMask, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue); - Assert.Equal (Key.PageDown | Key.CtrlMask, ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue); - Assert.Equal (Key.PageUp | Key.CtrlMask, ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue).KeyCode); Assert.False ((bool)ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue); } @@ -770,16 +767,16 @@ void ConfigurationManager_Applied (object sender, ConfigurationManagerEventArgs { fired = true; // assert - Assert.Equal (Key.Q, Application.QuitKey); - Assert.Equal (Key.F, Application.AlternateForwardKey); - Assert.Equal (Key.B, Application.AlternateBackwardKey); + Assert.Equal (KeyCode.Q, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.F, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.B, Application.AlternateBackwardKey.KeyCode); Assert.True (Application.IsMouseDisabled); } // act - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q; - ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F; - ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.Q); + ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Apply (); diff --git a/UnitTests/Configuration/JsonConverterTests.cs b/UnitTests/Configuration/JsonConverterTests.cs index ed5ff6d6dd..41a5650bb0 100644 --- a/UnitTests/Configuration/JsonConverterTests.cs +++ b/UnitTests/Configuration/JsonConverterTests.cs @@ -1,274 +1,332 @@ -using System.Text.Json; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Unicode; using Xunit; -namespace Terminal.Gui.ConfigurationTests { - public class ColorJsonConverterTests { - - [Theory] - [InlineData ("Black", Color.Black)] - [InlineData ("Blue", Color.Blue)] - [InlineData ("BrightBlue", Color.BrightBlue)] - [InlineData ("BrightCyan", Color.BrightCyan)] - [InlineData ("BrightGreen", Color.BrightGreen)] - [InlineData ("BrightMagenta", Color.BrightMagenta)] - [InlineData ("BrightRed", Color.BrightRed)] - [InlineData ("BrightYellow", Color.BrightYellow)] - [InlineData ("Yellow", Color.Yellow)] - [InlineData ("Cyan", Color.Cyan)] - [InlineData ("DarkGray", Color.DarkGray)] - [InlineData ("Gray", Color.Gray)] - [InlineData ("Green", Color.Green)] - [InlineData ("Magenta", Color.Magenta)] - [InlineData ("Red", Color.Red)] - [InlineData ("White", Color.White)] - public void TestColorDeserializationFromHumanReadableColorNames (string colorName, ColorName expectedColor) - { - // Arrange - string json = $"\"{colorName}\""; - - // Act - Color actualColor = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); - - // Assert - Assert.Equal (new Color (expectedColor), actualColor); - } - - [Theory] - [InlineData (ColorName.Black, "Black")] - [InlineData (ColorName.Blue, "Blue")] - [InlineData (ColorName.Green, "Green")] - [InlineData (ColorName.Cyan, "Cyan")] - [InlineData (ColorName.Gray, "Gray")] - [InlineData (ColorName.Red, "Red")] - [InlineData (ColorName.Magenta, "Magenta")] - [InlineData (ColorName.Yellow, "Yellow")] - [InlineData (ColorName.DarkGray, "DarkGray")] - [InlineData (ColorName.BrightBlue, "BrightBlue")] - [InlineData (ColorName.BrightGreen, "BrightGreen")] - [InlineData (ColorName.BrightCyan, "BrightCyan")] - [InlineData (ColorName.BrightRed, "BrightRed")] - [InlineData (ColorName.BrightMagenta, "BrightMagenta")] - [InlineData (ColorName.BrightYellow, "BrightYellow")] - [InlineData (ColorName.White, "White")] - public void SerializesEnumValuesAsStrings (ColorName colorName, string expectedJson) - { - var converter = new ColorJsonConverter (); - var options = new JsonSerializerOptions { Converters = { converter } }; - - var serialized = JsonSerializer.Serialize (new Color (colorName), options); - - Assert.Equal ($"\"{expectedJson}\"", serialized); - } - - [Fact] - public void TestSerializeColor_Black () - { - // Arrange - var expectedJson = "\"Black\""; - - // Act - var json = JsonSerializer.Serialize (new Color (Color.Black), new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - // Assert - Assert.Equal (expectedJson, json); - } - - [Fact] - public void TestSerializeColor_BrightRed () - { - // Arrange - var expectedJson = "\"BrightRed\""; - - // Act - var json = JsonSerializer.Serialize (new Color (Color.BrightRed), new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - // Assert - Assert.Equal (expectedJson, json); - } - - [Fact] - public void TestDeserializeColor_Black () - { - // Arrange - var json = "\"Black\""; - var expectedColor = new Color (ColorName.Black); - - // Act - var color = JsonSerializer.Deserialize (json, new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - // Assert - Assert.Equal (expectedColor, color); - } - - [Fact] - public void TestDeserializeColor_BrightRed () - { - // Arrange - var json = "\"BrightRed\""; - var expectedColor = new Color (ColorName.BrightRed); - - // Act - var color = JsonSerializer.Deserialize (json, new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - // Assert - Assert.Equal (expectedColor, color); - } - - [Theory] - [InlineData (0, 0, 0, "\"#000000\"")] - [InlineData (0, 0, 1, "\"#000001\"")] - public void SerializesToHexCode (int r, int g, int b, string expected) - { - // Arrange - - // Act - var actual = JsonSerializer.Serialize (new Color (r, g, b), new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - //Assert - Assert.Equal (expected, actual); - - } - - [Theory] - [InlineData ("\"#000000\"", 0, 0, 0)] - public void DeserializesFromHexCode (string hexCode, int r, int g, int b) - { - // Arrange - Color expected = new Color (r, g, b); - - // Act - var actual = JsonSerializer.Deserialize (hexCode, new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - //Assert - Assert.Equal (expected, actual); - } - - [Theory] - [InlineData ("\"rgb(0,0,0)\"", 0, 0, 0)] - public void DeserializesFromRgb (string rgb, int r, int g, int b) - { - // Arrange - Color expected = new Color (r, g, b); - - // Act - var actual = JsonSerializer.Deserialize (rgb, new JsonSerializerOptions { - Converters = { new ColorJsonConverter () } - }); - - //Assert - Assert.Equal (expected, actual); - } +namespace Terminal.Gui.ConfigurationTests; + +public class ColorJsonConverterTests { + [Theory] + [InlineData ("Black", Color.Black)] + [InlineData ("Blue", Color.Blue)] + [InlineData ("BrightBlue", Color.BrightBlue)] + [InlineData ("BrightCyan", Color.BrightCyan)] + [InlineData ("BrightGreen", Color.BrightGreen)] + [InlineData ("BrightMagenta", Color.BrightMagenta)] + [InlineData ("BrightRed", Color.BrightRed)] + [InlineData ("BrightYellow", Color.BrightYellow)] + [InlineData ("Yellow", Color.Yellow)] + [InlineData ("Cyan", Color.Cyan)] + [InlineData ("DarkGray", Color.DarkGray)] + [InlineData ("Gray", Color.Gray)] + [InlineData ("Green", Color.Green)] + [InlineData ("Magenta", Color.Magenta)] + [InlineData ("Red", Color.Red)] + [InlineData ("White", Color.White)] + public void TestColorDeserializationFromHumanReadableColorNames (string colorName, ColorName expectedColor) + { + // Arrange + string json = $"\"{colorName}\""; + + // Act + var actualColor = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); + + // Assert + Assert.Equal (new Color (expectedColor), actualColor); } - public class AttributeJsonConverterTests { - [Fact, AutoInitShutdown] - public void TestDeserialize () - { - // Test deserializing from human-readable color names - var json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}"; - var attribute = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); - Assert.Equal (Color.Blue, attribute.Foreground.ColorName); - Assert.Equal (Color.Green, attribute.Background.ColorName); - - // Test deserializing from RGB values - json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}"; - attribute = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); - Assert.Equal (Color.Red, attribute.Foreground.ColorName); - Assert.Equal (Color.BrightGreen, attribute.Background.ColorName); - } - - [Fact, AutoInitShutdown] - public void TestSerialize () - { - // Test serializing to human-readable color names - var attribute = new Attribute (Color.Blue, Color.Green); - var json = JsonSerializer.Serialize (attribute, ConfigurationManagerTests._jsonOptions); - Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json); - } + [Theory] + [InlineData (ColorName.Black, "Black")] + [InlineData (ColorName.Blue, "Blue")] + [InlineData (ColorName.Green, "Green")] + [InlineData (ColorName.Cyan, "Cyan")] + [InlineData (ColorName.Gray, "Gray")] + [InlineData (ColorName.Red, "Red")] + [InlineData (ColorName.Magenta, "Magenta")] + [InlineData (ColorName.Yellow, "Yellow")] + [InlineData (ColorName.DarkGray, "DarkGray")] + [InlineData (ColorName.BrightBlue, "BrightBlue")] + [InlineData (ColorName.BrightGreen, "BrightGreen")] + [InlineData (ColorName.BrightCyan, "BrightCyan")] + [InlineData (ColorName.BrightRed, "BrightRed")] + [InlineData (ColorName.BrightMagenta, "BrightMagenta")] + [InlineData (ColorName.BrightYellow, "BrightYellow")] + [InlineData (ColorName.White, "White")] + public void SerializesEnumValuesAsStrings (ColorName colorName, string expectedJson) + { + var converter = new ColorJsonConverter (); + var options = new JsonSerializerOptions { Converters = { converter } }; + + string serialized = JsonSerializer.Serialize (new Color (colorName), options); + + Assert.Equal ($"\"{expectedJson}\"", serialized); } - public class ColorSchemeJsonConverterTests { - //string json = @" - // { - // ""ColorSchemes"": { - // ""Base"": { - // ""normal"": { - // ""foreground"": ""White"", - // ""background"": ""Blue"" - // }, - // ""focus"": { - // ""foreground"": ""Black"", - // ""background"": ""Gray"" - // }, - // ""hotNormal"": { - // ""foreground"": ""BrightCyan"", - // ""background"": ""Blue"" - // }, - // ""hotFocus"": { - // ""foreground"": ""BrightBlue"", - // ""background"": ""Gray"" - // }, - // ""disabled"": { - // ""foreground"": ""DarkGray"", - // ""background"": ""Blue"" - // } - // } - // } - // }"; - [Fact, AutoInitShutdown] - public void TestColorSchemesSerialization () - { - // Arrange - var expectedColorScheme = new ColorScheme { - Normal = new Attribute (Color.White, Color.Blue), - Focus = new Attribute (Color.Black, Color.Gray), - HotNormal = new Attribute (Color.BrightCyan, Color.Blue), - HotFocus = new Attribute (Color.BrightBlue, Color.Gray), - Disabled = new Attribute (Color.DarkGray, Color.Blue) - }; - var serializedColorScheme = JsonSerializer.Serialize (expectedColorScheme, ConfigurationManagerTests._jsonOptions); - - // Act - var actualColorScheme = JsonSerializer.Deserialize (serializedColorScheme, ConfigurationManagerTests._jsonOptions); - - // Assert - Assert.Equal (expectedColorScheme, actualColorScheme); - } + [Fact] + public void TestSerializeColor_Black () + { + // Arrange + string expectedJson = "\"Black\""; + + // Act + string json = JsonSerializer.Serialize (new Color (Color.Black), new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + // Assert + Assert.Equal (expectedJson, json); + } + + [Fact] + public void TestSerializeColor_BrightRed () + { + // Arrange + string expectedJson = "\"BrightRed\""; + + // Act + string json = JsonSerializer.Serialize (new Color (Color.BrightRed), new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + // Assert + Assert.Equal (expectedJson, json); + } + + [Fact] + public void TestDeserializeColor_Black () + { + // Arrange + string json = "\"Black\""; + var expectedColor = new Color (ColorName.Black); + + // Act + var color = JsonSerializer.Deserialize (json, new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + // Assert + Assert.Equal (expectedColor, color); + } + + [Fact] + public void TestDeserializeColor_BrightRed () + { + // Arrange + string json = "\"BrightRed\""; + var expectedColor = new Color (ColorName.BrightRed); + + // Act + var color = JsonSerializer.Deserialize (json, new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + // Assert + Assert.Equal (expectedColor, color); + } + + [Theory] + [InlineData (0, 0, 0, "\"#000000\"")] + [InlineData (0, 0, 1, "\"#000001\"")] + public void SerializesToHexCode (int r, int g, int b, string expected) + { + // Arrange + + // Act + string actual = JsonSerializer.Serialize (new Color (r, g, b), new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + //Assert + Assert.Equal (expected, actual); + + } + + [Theory] + [InlineData ("\"#000000\"", 0, 0, 0)] + public void DeserializesFromHexCode (string hexCode, int r, int g, int b) + { + // Arrange + var expected = new Color (r, g, b); + + // Act + var actual = JsonSerializer.Deserialize (hexCode, new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + //Assert + Assert.Equal (expected, actual); + } + + [Theory] + [InlineData ("\"rgb(0,0,0)\"", 0, 0, 0)] + public void DeserializesFromRgb (string rgb, int r, int g, int b) + { + // Arrange + var expected = new Color (r, g, b); + + // Act + var actual = JsonSerializer.Deserialize (rgb, new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); + + //Assert + Assert.Equal (expected, actual); + } +} + +public class AttributeJsonConverterTests { + [Fact] + public void TestDeserialize () + { + // Test deserializing from human-readable color names + string json = "{\"Foreground\":\"Blue\",\"Background\":\"Green\"}"; + var attribute = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); + Assert.Equal (Color.Blue, attribute.Foreground.ColorName); + Assert.Equal (Color.Green, attribute.Background.ColorName); + + // Test deserializing from RGB values + json = "{\"Foreground\":\"rgb(255,0,0)\",\"Background\":\"rgb(0,255,0)\"}"; + attribute = JsonSerializer.Deserialize (json, ConfigurationManagerTests._jsonOptions); + Assert.Equal (Color.Red, attribute.Foreground.ColorName); + Assert.Equal (Color.BrightGreen, attribute.Background.ColorName); + } + + [Fact] [AutoInitShutdown] + public void TestSerialize () + { + // Test serializing to human-readable color names + var attribute = new Attribute (Color.Blue, Color.Green); + string json = JsonSerializer.Serialize (attribute, ConfigurationManagerTests._jsonOptions); + Assert.Equal ("{\"Foreground\":\"Blue\",\"Background\":\"Green\"}", json); + } +} + +public class ColorSchemeJsonConverterTests { + //string json = @" + // { + // ""ColorSchemes"": { + // ""Base"": { + // ""normal"": { + // ""foreground"": ""White"", + // ""background"": ""Blue"" + // }, + // ""focus"": { + // ""foreground"": ""Black"", + // ""background"": ""Gray"" + // }, + // ""hotNormal"": { + // ""foreground"": ""BrightCyan"", + // ""background"": ""Blue"" + // }, + // ""hotFocus"": { + // ""foreground"": ""BrightBlue"", + // ""background"": ""Gray"" + // }, + // ""disabled"": { + // ""foreground"": ""DarkGray"", + // ""background"": ""Blue"" + // } + // } + // } + // }"; + [Fact] [AutoInitShutdown] + public void TestColorSchemesSerialization () + { + // Arrange + var expectedColorScheme = new ColorScheme { + Normal = new Attribute (Color.White, Color.Blue), + Focus = new Attribute (Color.Black, Color.Gray), + HotNormal = new Attribute (Color.BrightCyan, Color.Blue), + HotFocus = new Attribute (Color.BrightBlue, Color.Gray), + Disabled = new Attribute (Color.DarkGray, Color.Blue) + }; + string serializedColorScheme = JsonSerializer.Serialize (expectedColorScheme, ConfigurationManagerTests._jsonOptions); + + // Act + var actualColorScheme = JsonSerializer.Deserialize (serializedColorScheme, ConfigurationManagerTests._jsonOptions); + + // Assert + Assert.Equal (expectedColorScheme, actualColorScheme); + } +} + +public class KeyCodeJsonConverterTests { + [Theory] + [InlineData (KeyCode.A, "A")] + [InlineData (KeyCode.A | KeyCode.ShiftMask, "A, ShiftMask")] + [InlineData (KeyCode.A | KeyCode.CtrlMask, "A, CtrlMask")] + [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "A, CtrlMask, AltMask")] + [InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "Space, A, CtrlMask, AltMask")] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "Space, A, ShiftMask")] + [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Delete, CtrlMask, AltMask")] + [InlineData (KeyCode.D4, "D4")] + [InlineData (KeyCode.Esc, "Esc")] + public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo) + { + // Arrange + var options = new JsonSerializerOptions (); + options.Converters.Add (new KeyCodeJsonConverter ()); + + // Act + string json = JsonSerializer.Serialize (key, options); + var deserializedKey = JsonSerializer.Deserialize (json, options); + + // Assert + Assert.Equal (expectedStringTo, deserializedKey.ToString ()); + } +} + +public class KeyJsonConverterTests { + [Theory] + [InlineData (KeyCode.A, "{\"Key\":\"a\"}")] + [InlineData ((KeyCode)'â', "{\"Key\":\"â\"}")] + [InlineData (KeyCode.A | KeyCode.ShiftMask, "{\"Key\":\"A\"}")] + [InlineData (KeyCode.A | KeyCode.CtrlMask, "{\"Key\":\"Ctrl+A\"}")] + [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "{\"Key\":\"Ctrl+Alt+A\"}")] + [InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "{\"Key\":\"Ctrl+Alt+A\"}")] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "{\"Key\":\"A\"}")] + [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "{\"Key\":\"Ctrl+Alt+Delete\"}")] + [InlineData (KeyCode.D4, "{\"Key\":\"4\"}")] + [InlineData (KeyCode.Esc, "{\"Key\":\"Esc\"}")] + public void TestKey_Serialize (KeyCode key, string expected) + { + // Arrange + var options = new JsonSerializerOptions (); + options.Converters.Add (new KeyJsonConverter ()); + options.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + + // Act + string json = JsonSerializer.Serialize ((Key)key, options); + + // Assert + Assert.Equal (expected, json); } - public class KeyJsonConverterTests { - [Theory, AutoInitShutdown] - [InlineData (Key.A, "A")] - [InlineData (Key.a | Key.ShiftMask, "a, ShiftMask")] - [InlineData (Key.A | Key.CtrlMask, "A, CtrlMask")] - [InlineData (Key.a | Key.AltMask | Key.CtrlMask, "a, CtrlMask, AltMask")] - [InlineData (Key.Delete | Key.AltMask | Key.CtrlMask, "Delete, CtrlMask, AltMask")] - [InlineData (Key.D4, "D4")] - [InlineData (Key.Esc, "Esc")] - public void TestKeyRoundTripConversion (Key key, string expectedStringTo) - { - // Arrange - var options = new JsonSerializerOptions (); - options.Converters.Add (new KeyJsonConverter ()); - - // Act - var json = JsonSerializer.Serialize (key, options); - var deserializedKey = JsonSerializer.Deserialize (json, options); - - // Assert - Assert.Equal (expectedStringTo, deserializedKey.ToString ()); - } + [Theory] + [InlineData (KeyCode.A, "a")] + [InlineData (KeyCode.A | KeyCode.ShiftMask, "A")] + [InlineData (KeyCode.A | KeyCode.CtrlMask, "Ctrl+A")] + [InlineData (KeyCode.A | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+A")] + [InlineData ((KeyCode)'a' | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+A")] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "A")] + [InlineData (KeyCode.Delete | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Delete")] + [InlineData (KeyCode.D4, "4")] + [InlineData (KeyCode.Esc, "Esc")] + public void TestKeyRoundTripConversion (KeyCode key, string expectedStringTo) + { + // Arrange + var options = new JsonSerializerOptions (); + options.Converters.Add (new KeyJsonConverter ()); + var encoderSettings = new TextEncoderSettings (); + encoderSettings.AllowCharacters ('+', '-'); + encoderSettings.AllowRange (UnicodeRanges.BasicLatin); + options.Encoder = JavaScriptEncoder.Create (encoderSettings); + + // Act + string json = JsonSerializer.Serialize ((Key)key, options); + var deserializedKey = JsonSerializer.Deserialize (json, options); + + // Assert + Assert.Equal (expectedStringTo, deserializedKey.ToString ()); } } \ No newline at end of file diff --git a/UnitTests/Configuration/SettingsScopeTests.cs b/UnitTests/Configuration/SettingsScopeTests.cs index b7d89f258d..571f302c63 100644 --- a/UnitTests/Configuration/SettingsScopeTests.cs +++ b/UnitTests/Configuration/SettingsScopeTests.cs @@ -38,42 +38,42 @@ public void GetHardCodedDefaults_ShouldSetProperties () public void Apply_ShouldApplyProperties () { // arrange - Assert.Equal (Key.Q | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue); - Assert.Equal (Key.PageDown | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue); - Assert.Equal (Key.PageUp | Key.CtrlMask, (Key)ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue); + Assert.Equal (KeyCode.Q | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.PageDown | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.PageUp | KeyCode.CtrlMask, ((Key)ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue).KeyCode); Assert.False ((bool)ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue); // act - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.Q; - ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = Key.F; - ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = Key.B; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.Q); + ConfigurationManager.Settings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + ConfigurationManager.Settings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); ConfigurationManager.Settings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Settings.Apply (); // assert - Assert.Equal (Key.Q, Application.QuitKey); - Assert.Equal (Key.F, Application.AlternateForwardKey); - Assert.Equal (Key.B, Application.AlternateBackwardKey); + Assert.Equal (KeyCode.Q, Application.QuitKey.KeyCode); + Assert.Equal (KeyCode.F, Application.AlternateForwardKey.KeyCode); + Assert.Equal (KeyCode.B, Application.AlternateBackwardKey.KeyCode); Assert.True (Application.IsMouseDisabled); } [Fact, AutoInitShutdown] public void CopyUpdatedProperitesFrom_ShouldCopyChangedPropertiesOnly () { - ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = Key.End; + ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue = new Key (KeyCode.End); ; var updatedSettings = new SettingsScope (); ///Don't set Quitkey - updatedSettings["Application.AlternateForwardKey"].PropertyValue = Key.F; - updatedSettings["Application.AlternateBackwardKey"].PropertyValue = Key.B; - updatedSettings["Application.IsMouseDisabled"].PropertyValue = true; + updatedSettings ["Application.AlternateForwardKey"].PropertyValue = new Key (KeyCode.F); + updatedSettings ["Application.AlternateBackwardKey"].PropertyValue = new Key (KeyCode.B); + updatedSettings ["Application.IsMouseDisabled"].PropertyValue = true; ConfigurationManager.Settings.Update (updatedSettings); - Assert.Equal (Key.End, ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue); - Assert.Equal (Key.F, updatedSettings ["Application.AlternateForwardKey"].PropertyValue); - Assert.Equal (Key.B, updatedSettings ["Application.AlternateBackwardKey"].PropertyValue); + Assert.Equal (KeyCode.End, ((Key)ConfigurationManager.Settings ["Application.QuitKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.F, ((Key)updatedSettings ["Application.AlternateForwardKey"].PropertyValue).KeyCode); + Assert.Equal (KeyCode.B, ((Key)updatedSettings ["Application.AlternateBackwardKey"].PropertyValue).KeyCode); Assert.True ((bool)updatedSettings ["Application.IsMouseDisabled"].PropertyValue); } } diff --git a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs index e54a6e1b57..d3c09b80e2 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -57,11 +57,13 @@ public void FakeDriver_Only_Sends_Keystrokes_Through_MockKeyPresses (Type driver Application.Init (driver); var top = Application.Top; - var view = new View (); + var view = new View () { + CanFocus = true + }; var count = 0; var wasKeyPressed = false; - view.KeyPressed += (s, e) => { + view.KeyDown += (s, e) => { wasKeyPressed = true; }; top.Add (view); @@ -96,13 +98,15 @@ public void FakeDriver_MockKeyPresses (Type driverType) Console.MockKeyPresses = mKeys; var top = Application.Top; - var view = new View (); + var view = new View () { + CanFocus = true + }; var rText = ""; var idx = 0; - view.KeyPressed += (s, e) => { - Assert.Equal (text [idx], (char)e.KeyEvent.Key); - rText += (char)e.KeyEvent.Key; + view.KeyDown += (s, e) => { + Assert.Equal (text [idx], (char)e.KeyCode); + rText += (char)e.KeyCode; Assert.Equal (rText, text.Substring (0, idx + 1)); e.Handled = true; idx++; @@ -155,7 +159,7 @@ public void FakeDriver_MockKeyPresses (Type driverType) // Key key = Key.Unknown; // Application.Top.KeyPress += (e) => { - // key = e.KeyEvent.Key; + // key = e.Key; // output.WriteLine ($" Application.Top.KeyPress: {key}"); // e.Handled = true; @@ -239,7 +243,7 @@ public void TerminalResized_Simulation (Type driverType) // var pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, output); // Assert.Equal (new Rect (0, 0, 20, 8), pos); - // Assert.True (dlg.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + // Assert.True (dlg.ProcessKey (new (Key.Tab))); // dlg.Draw (); // expected = @" diff --git a/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs b/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs new file mode 100644 index 0000000000..26cb60ec5a --- /dev/null +++ b/UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Xunit; + +namespace Terminal.Gui.ConsoleDrivers; +public class ConsoleKeyMappingTests { + [Theory] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, ConsoleKey.A, KeyCode.A, 'A')] + [InlineData ((KeyCode)'A', ConsoleKey.A, (KeyCode)'a', 'a')] + [InlineData ((KeyCode)'à' | KeyCode.ShiftMask, ConsoleKey.A, (KeyCode)'À', 'À')] + [InlineData ((KeyCode)'À', ConsoleKey.A, (KeyCode)'à', 'à')] + [InlineData ((KeyCode)'ü' | KeyCode.ShiftMask, ConsoleKey.U, (KeyCode)'Ü', 'Ü')] + [InlineData ((KeyCode)'Ü', ConsoleKey.U, (KeyCode)'ü', 'ü')] + [InlineData ((KeyCode)'ý' | KeyCode.ShiftMask, ConsoleKey.Y, (KeyCode)'Ý', 'Ý')] + [InlineData ((KeyCode)'Ý', ConsoleKey.Y, (KeyCode)'ý', 'ý')] + [InlineData ((KeyCode)'!' | KeyCode.ShiftMask, ConsoleKey.D1, (KeyCode)'!', '!')] + [InlineData (KeyCode.D1, ConsoleKey.D1, KeyCode.D1, '1')] + [InlineData ((KeyCode)'/' | KeyCode.ShiftMask, ConsoleKey.D7, (KeyCode)'/', '/')] + [InlineData (KeyCode.D7, ConsoleKey.D7, KeyCode.D7, '7')] + [InlineData (KeyCode.PageDown | KeyCode.ShiftMask, ConsoleKey.PageDown, KeyCode.Null, '\0')] + [InlineData (KeyCode.PageDown, ConsoleKey.PageDown, KeyCode.Null, '\0')] + + public void TestIfEqual (KeyCode key, ConsoleKey expectedConsoleKey, KeyCode expectedKey, char expectedChar) + { + var consoleKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey (key); + Assert.Equal (consoleKeyInfo.Key, expectedConsoleKey); + Assert.Equal ((char)expectedKey, expectedChar); + Assert.Equal (consoleKeyInfo.KeyChar, expectedChar); + } + + static object packetLock = new object (); + + /// + /// Sometimes when using remote tools EventKeyRecord sends 'virtual keystrokes'. + /// These are indicated with the wVirtualKeyCode of 231. When we see this code + /// then we need to look to the unicode character (UnicodeChar) instead of the key + /// when telling the rest of the framework what button was pressed. For full details + /// see: https://github.com/gui-cs/Terminal.Gui/issues/2008 + /// + [Theory] + [AutoInitShutdown] + [ClassData (typeof (PacketTest))] + public void TestVKPacket (uint unicodeCharacter, bool shift, bool alt, bool control, uint initialVirtualKey, + uint initialScanCode, KeyCode expectedRemapping, uint expectedVirtualKey, uint expectedScanCode) + { + lock (packetLock) { + Application._forceFakeConsole = true; + Application.Init (); + + var modifiers = new ConsoleModifiers (); + if (shift) { + modifiers |= ConsoleModifiers.Shift; + } + if (alt) { + modifiers |= ConsoleModifiers.Alt; + } + if (control) { + modifiers |= ConsoleModifiers.Control; + } + ConsoleKeyInfo consoleKeyInfo = ConsoleKeyMapping.GetConsoleKeyFromKey (unicodeCharacter, modifiers, out uint scanCode); + + Assert.Equal ((uint)consoleKeyInfo.Key, initialVirtualKey); + + + if (scanCode > 0 && consoleKeyInfo.KeyChar == 0) { + Assert.Equal (0, (double)consoleKeyInfo.KeyChar); + } else { + Assert.Equal (consoleKeyInfo.KeyChar, unicodeCharacter); + } + Assert.Equal ((uint)consoleKeyInfo.Key, expectedVirtualKey); + Assert.Equal (scanCode, initialScanCode); + Assert.Equal (scanCode, expectedScanCode); + + var top = Application.Top; + + top.KeyDown += (s, e) => { + Assert.Equal (Key.ToString (expectedRemapping), Key.ToString (e.KeyCode)); + e.Handled = true; + Application.RequestStop (); + }; + + int iterations = -1; + + Application.Iteration += (s, a) => { + iterations++; + if (iterations == 0) { + Application.Driver.SendKeys (consoleKeyInfo.KeyChar, ConsoleKey.Packet, shift, alt, control); + } + }; + Application.Run (); + Application.Shutdown (); + } + } + + public class PacketTest : IEnumerable, IEnumerable { + public IEnumerator GetEnumerator () + { + lock (packetLock) { + // unicodeCharacter, shift, alt, control, initialVirtualKey, initialScanCode, expectedRemapping, expectedVirtualKey, expectedScanCode + yield return new object [] { 'a', false, false, false, 'A', 30, KeyCode.A, 'A', 30 }; + yield return new object [] { 'A', true, false, false, 'A', 30, KeyCode.A | KeyCode.ShiftMask, 'A', 30 }; + yield return new object [] { 'A', true, true, false, 'A', 30, KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask, 'A', 30 }; + yield return new object [] { 'A', true, true, true, 'A', 30, KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 'A', 30 }; + yield return new object [] { 'z', false, false, false, 'Z', 44, KeyCode.Z, 'Z', 44 }; + yield return new object [] { 'Z', true, false, false, 'Z', 44, KeyCode.Z | KeyCode.ShiftMask, 'Z', 44 }; + yield return new object [] { 'Z', true, true, false, 'Z', 44, KeyCode.Z | KeyCode.ShiftMask | KeyCode.AltMask, 'Z', 44 }; + yield return new object [] { 'Z', true, true, true, 'Z', 44, KeyCode.Z | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 'Z', 44 }; + yield return new object [] { '英', false, false, false, '\0', 0, (KeyCode)'英', '\0', 0 }; + yield return new object [] { '英', true, false, false, '\0', 0, (KeyCode)'英' | KeyCode.ShiftMask, '\0', 0 }; + yield return new object [] { '英', true, true, false, '\0', 0, (KeyCode)'英' | KeyCode.ShiftMask | KeyCode.AltMask, '\0', 0 }; + yield return new object [] { '英', true, true, true, '\0', 0, (KeyCode)'英' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '\0', 0 }; + yield return new object [] { '+', false, false, false, 187, 26, (KeyCode)'+', 187, 26 }; + yield return new object [] { '*', true, false, false, 187, 26, (KeyCode)'*' | KeyCode.ShiftMask, 187, 26 }; + yield return new object [] { '+', true, true, false, 187, 26, (KeyCode)'+' | KeyCode.ShiftMask | KeyCode.AltMask, 187, 26 }; + yield return new object [] { '+', true, true, true, 187, 26, (KeyCode)'+' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 187, 26 }; + yield return new object [] { '1', false, false, false, '1', 2, KeyCode.D1, '1', 2 }; + yield return new object [] { '!', true, false, false, '1', 2, (KeyCode)'!' | KeyCode.ShiftMask, '1', 2 }; + yield return new object [] { '1', true, true, false, '1', 2, KeyCode.D1 | KeyCode.ShiftMask | KeyCode.AltMask, '1', 2 }; + yield return new object [] { '1', true, true, true, '1', 2, KeyCode.D1 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '1', 2 }; + yield return new object [] { '1', false, true, true, '1', 2, KeyCode.D1 | KeyCode.AltMask | KeyCode.CtrlMask, '1', 2 }; + yield return new object [] { '2', false, false, false, '2', 3, KeyCode.D2, '2', 3 }; + yield return new object [] { '"', true, false, false, '2', 3, (KeyCode)'"' | KeyCode.ShiftMask, '2', 3 }; + yield return new object [] { '2', true, true, false, '2', 3, KeyCode.D2 | KeyCode.ShiftMask | KeyCode.AltMask, '2', 3 }; + yield return new object [] { '2', true, true, true, '2', 3, KeyCode.D2 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '2', 3 }; + yield return new object [] { '@', false, true, true, '2', 3, (KeyCode)'@' | KeyCode.AltMask | KeyCode.CtrlMask, '2', 3 }; + yield return new object [] { '3', false, false, false, '3', 4, KeyCode.D3, '3', 4 }; + yield return new object [] { '#', true, false, false, '3', 4, (KeyCode)'#' | KeyCode.ShiftMask, '3', 4 }; + yield return new object [] { '3', true, true, false, '3', 4, KeyCode.D3 | KeyCode.ShiftMask | KeyCode.AltMask, '3', 4 }; + yield return new object [] { '3', true, true, true, '3', 4, KeyCode.D3 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '3', 4 }; + yield return new object [] { '£', false, true, true, '3', 4, (KeyCode)'£' | KeyCode.AltMask | KeyCode.CtrlMask, '3', 4 }; + yield return new object [] { '4', false, false, false, '4', 5, KeyCode.D4, '4', 5 }; + yield return new object [] { '$', true, false, false, '4', 5, (KeyCode)'$' | KeyCode.ShiftMask, '4', 5 }; + yield return new object [] { '4', true, true, false, '4', 5, KeyCode.D4 | KeyCode.ShiftMask | KeyCode.AltMask, '4', 5 }; + yield return new object [] { '4', true, true, true, '4', 5, KeyCode.D4 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '4', 5 }; + yield return new object [] { '§', false, true, true, '4', 5, (KeyCode)'§' | KeyCode.AltMask | KeyCode.CtrlMask, '4', 5 }; + yield return new object [] { '5', false, false, false, '5', 6, KeyCode.D5, '5', 6 }; + yield return new object [] { '%', true, false, false, '5', 6, (KeyCode)'%' | KeyCode.ShiftMask, '5', 6 }; + yield return new object [] { '5', true, true, false, '5', 6, KeyCode.D5 | KeyCode.ShiftMask | KeyCode.AltMask, '5', 6 }; + yield return new object [] { '5', true, true, true, '5', 6, KeyCode.D5 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '5', 6 }; + yield return new object [] { '€', false, true, true, '5', 6, (KeyCode)'€' | KeyCode.AltMask | KeyCode.CtrlMask, '5', 6 }; + yield return new object [] { '6', false, false, false, '6', 7, KeyCode.D6, '6', 7 }; + yield return new object [] { '&', true, false, false, '6', 7, (KeyCode)'&' | KeyCode.ShiftMask, '6', 7 }; + yield return new object [] { '6', true, true, false, '6', 7, KeyCode.D6 | KeyCode.ShiftMask | KeyCode.AltMask, '6', 7 }; + yield return new object [] { '6', true, true, true, '6', 7, KeyCode.D6 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '6', 7 }; + yield return new object [] { '6', false, true, true, '6', 7, KeyCode.D6 | KeyCode.AltMask | KeyCode.CtrlMask, '6', 7 }; + yield return new object [] { '7', false, false, false, '7', 8, KeyCode.D7, '7', 8 }; + yield return new object [] { '/', true, false, false, '7', 8, (KeyCode)'/' | KeyCode.ShiftMask, '7', 8 }; + yield return new object [] { '7', true, true, false, '7', 8, KeyCode.D7 | KeyCode.ShiftMask | KeyCode.AltMask, '7', 8 }; + yield return new object [] { '7', true, true, true, '7', 8, KeyCode.D7 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '7', 8 }; + yield return new object [] { '{', false, true, true, '7', 8, (KeyCode)'{' | KeyCode.AltMask | KeyCode.CtrlMask, '7', 8 }; + yield return new object [] { '8', false, false, false, '8', 9, KeyCode.D8, '8', 9 }; + yield return new object [] { '(', true, false, false, '8', 9, (KeyCode)'(' | KeyCode.ShiftMask, '8', 9 }; + yield return new object [] { '8', true, true, false, '8', 9, KeyCode.D8 | KeyCode.ShiftMask | KeyCode.AltMask, '8', 9 }; + yield return new object [] { '8', true, true, true, '8', 9, KeyCode.D8 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '8', 9 }; + yield return new object [] { '[', false, true, true, '8', 9, (KeyCode)'[' | KeyCode.AltMask | KeyCode.CtrlMask, '8', 9 }; + yield return new object [] { '9', false, false, false, '9', 10, KeyCode.D9, '9', 10 }; + yield return new object [] { ')', true, false, false, '9', 10, (KeyCode)')' | KeyCode.ShiftMask, '9', 10 }; + yield return new object [] { '9', true, true, false, '9', 10, KeyCode.D9 | KeyCode.ShiftMask | KeyCode.AltMask, '9', 10 }; + yield return new object [] { '9', true, true, true, '9', 10, KeyCode.D9 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '9', 10 }; + yield return new object [] { ']', false, true, true, '9', 10, (KeyCode)']' | KeyCode.AltMask | KeyCode.CtrlMask, '9', 10 }; + yield return new object [] { '0', false, false, false, '0', 11, KeyCode.D0, '0', 11 }; + yield return new object [] { '=', true, false, false, '0', 11, (KeyCode)'=' | KeyCode.ShiftMask, '0', 11 }; + yield return new object [] { '0', true, true, false, '0', 11, KeyCode.D0 | KeyCode.ShiftMask | KeyCode.AltMask, '0', 11 }; + yield return new object [] { '0', true, true, true, '0', 11, KeyCode.D0 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '0', 11 }; + yield return new object [] { '}', false, true, true, '0', 11, (KeyCode)'}' | KeyCode.AltMask | KeyCode.CtrlMask, '0', 11 }; + yield return new object [] { '\'', false, false, false, 219, 12, (KeyCode)'\'', 219, 12 }; + yield return new object [] { '?', true, false, false, 219, 12, (KeyCode)'?' | KeyCode.ShiftMask, 219, 12 }; + yield return new object [] { '\'', true, true, false, 219, 12, (KeyCode)'\'' | KeyCode.ShiftMask | KeyCode.AltMask, 219, 12 }; + yield return new object [] { '\'', true, true, true, 219, 12, (KeyCode)'\'' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 219, 12 }; + yield return new object [] { '«', false, false, false, 221, 13, (KeyCode)'«', 221, 13 }; + yield return new object [] { '»', true, false, false, 221, 13, (KeyCode)'»' | KeyCode.ShiftMask, 221, 13 }; + yield return new object [] { '«', true, true, false, 221, 13, (KeyCode)'«' | KeyCode.ShiftMask | KeyCode.AltMask, 221, 13 }; + yield return new object [] { '«', true, true, true, 221, 13, (KeyCode)'«' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 221, 13 }; + yield return new object [] { 'á', false, false, false, 'A', 30, (KeyCode)'á', 'A', 30 }; + yield return new object [] { 'Á', true, false, false, 'A', 30, (KeyCode)'Á' | KeyCode.ShiftMask, 'A', 30 }; + yield return new object [] { 'à', false, false, false, 'A', 30, (KeyCode)'à', 'A', 30 }; + yield return new object [] { 'À', true, false, false, 'A', 30, (KeyCode)'À' | KeyCode.ShiftMask, 'A', 30 }; + yield return new object [] { 'é', false, false, false, 'E', 18, (KeyCode)'é', 'E', 18 }; + yield return new object [] { 'É', true, false, false, 'E', 18, (KeyCode)'É' | KeyCode.ShiftMask, 'E', 18 }; + yield return new object [] { 'è', false, false, false, 'E', 18, (KeyCode)'è', 'E', 18 }; + yield return new object [] { 'È', true, false, false, 'E', 18, (KeyCode)'È' | KeyCode.ShiftMask, 'E', 18 }; + yield return new object [] { 'í', false, false, false, 'I', 23, (KeyCode)'í', 'I', 23 }; + yield return new object [] { 'Í', true, false, false, 'I', 23, (KeyCode)'Í' | KeyCode.ShiftMask, 'I', 23 }; + yield return new object [] { 'ì', false, false, false, 'I', 23, (KeyCode)'ì', 'I', 23 }; + yield return new object [] { 'Ì', true, false, false, 'I', 23, (KeyCode)'Ì' | KeyCode.ShiftMask, 'I', 23 }; + yield return new object [] { 'ó', false, false, false, 'O', 24, (KeyCode)'ó', 'O', 24 }; + yield return new object [] { 'Ó', true, false, false, 'O', 24, (KeyCode)'Ó' | KeyCode.ShiftMask, 'O', 24 }; + yield return new object [] { 'ò', false, false, false, 'O', 24, (KeyCode)'ò', 'O', 24 }; + yield return new object [] { 'Ò', true, false, false, 'O', 24, (KeyCode)'Ò' | KeyCode.ShiftMask, 'O', 24 }; + yield return new object [] { 'ú', false, false, false, 'U', 22, (KeyCode)'ú', 'U', 22 }; + yield return new object [] { 'Ú', true, false, false, 'U', 22, (KeyCode)'Ú' | KeyCode.ShiftMask, 'U', 22 }; + yield return new object [] { 'ù', false, false, false, 'U', 22, (KeyCode)'ù', 'U', 22 }; + yield return new object [] { 'Ù', true, false, false, 'U', 22, (KeyCode)'Ù' | KeyCode.ShiftMask, 'U', 22 }; + yield return new object [] { 'ö', false, false, false, 'O', 24, (KeyCode)'ö', 'O', 24 }; + yield return new object [] { 'Ö', true, false, false, 'O', 24, (KeyCode)'Ö' | KeyCode.ShiftMask, 'O', 24 }; + yield return new object [] { '<', false, false, false, 226, 86, (KeyCode)'<', 226, 86 }; + yield return new object [] { '>', true, false, false, 226, 86, (KeyCode)'>' | KeyCode.ShiftMask, 226, 86 }; + yield return new object [] { '<', true, true, false, 226, 86, (KeyCode)'<' | KeyCode.ShiftMask | KeyCode.AltMask, 226, 86 }; + yield return new object [] { '<', true, true, true, 226, 86, (KeyCode)'<' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 226, 86 }; + yield return new object [] { 'ç', false, false, false, 192, 39, (KeyCode)'ç', 192, 39 }; + yield return new object [] { 'Ç', true, false, false, 192, 39, (KeyCode)'Ç' | KeyCode.ShiftMask, 192, 39 }; + yield return new object [] { 'ç', true, true, false, 192, 39, (KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask, 192, 39 }; + yield return new object [] { 'ç', true, true, true, 192, 39, (KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 192, 39 }; + yield return new object [] { '¨', false, true, true, 187, 26, (KeyCode)'¨' | KeyCode.AltMask | KeyCode.CtrlMask, 187, 26 }; + yield return new object [] { KeyCode.PageUp, false, false, false, 33, 73, KeyCode.Null, 33, 73 }; + yield return new object [] { KeyCode.PageUp, true, false, false, 33, 73, KeyCode.Null | KeyCode.ShiftMask, 33, 73 }; + yield return new object [] { KeyCode.PageUp, true, true, false, 33, 73, KeyCode.Null | KeyCode.ShiftMask | KeyCode.AltMask, 33, 73 }; + yield return new object [] { KeyCode.PageUp, true, true, true, 33, 73, KeyCode.Null | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, 33, 73 }; + } + } + + IEnumerator IEnumerable.GetEnumerator () + { + return GetEnumerator (); + } + } +} diff --git a/UnitTests/ConsoleDrivers/KeyCodeTests.cs b/UnitTests/ConsoleDrivers/KeyCodeTests.cs new file mode 100644 index 0000000000..ef1801ecd8 --- /dev/null +++ b/UnitTests/ConsoleDrivers/KeyCodeTests.cs @@ -0,0 +1,181 @@ +using System; +using Xunit; +using Xunit.Abstractions; + +namespace Terminal.Gui.DriverTests; + +public class KeyCodeTests { + readonly ITestOutputHelper _output; + + public KeyCodeTests (ITestOutputHelper output) + { + _output = output; + } + + enum SimpleEnum { Zero, One, Two, Three, Four, Five } + + [Flags] + enum FlaggedEnum { Zero, One, Two, Three, Four, Five } + + enum SimpleHighValueEnum { Zero, One, Two, Three, Four, Last = 0x40000000 } + + [Flags] + enum FlaggedHighValueEnum { Zero, One, Two, Three, Four, Last = 0x40000000 } + + [Fact] + public void SimpleEnum_And_FlagedEnum () + { + var simple = SimpleEnum.Three | SimpleEnum.Five; + + // Nothing will not be well compared here. + Assert.True (simple.HasFlag (SimpleEnum.Zero | SimpleEnum.Five)); + Assert.True (simple.HasFlag (SimpleEnum.One | SimpleEnum.Five)); + Assert.True (simple.HasFlag (SimpleEnum.Two | SimpleEnum.Five)); + Assert.True (simple.HasFlag (SimpleEnum.Three | SimpleEnum.Five)); + Assert.True (simple.HasFlag (SimpleEnum.Four | SimpleEnum.Five)); + Assert.True ((simple & (SimpleEnum.Zero | SimpleEnum.Five)) != 0); + Assert.True ((simple & (SimpleEnum.One | SimpleEnum.Five)) != 0); + Assert.True ((simple & (SimpleEnum.Two | SimpleEnum.Five)) != 0); + Assert.True ((simple & (SimpleEnum.Three | SimpleEnum.Five)) != 0); + Assert.True ((simple & (SimpleEnum.Four | SimpleEnum.Five)) != 0); + Assert.Equal (7, (int)simple); // As it is not flagged only shows as number. + Assert.Equal ("7", simple.ToString ()); + Assert.False (simple == (SimpleEnum.Zero | SimpleEnum.Five)); + Assert.False (simple == (SimpleEnum.One | SimpleEnum.Five)); + Assert.True (simple == (SimpleEnum.Two | SimpleEnum.Five)); + Assert.True (simple == (SimpleEnum.Three | SimpleEnum.Five)); + Assert.False (simple == (SimpleEnum.Four | SimpleEnum.Five)); + + var flagged = FlaggedEnum.Three | FlaggedEnum.Five; + + // Nothing will not be well compared here. + Assert.True (flagged.HasFlag (FlaggedEnum.Zero | FlaggedEnum.Five)); + Assert.True (flagged.HasFlag (FlaggedEnum.One | FlaggedEnum.Five)); + Assert.True (flagged.HasFlag (FlaggedEnum.Two | FlaggedEnum.Five)); + Assert.True (flagged.HasFlag (FlaggedEnum.Three | FlaggedEnum.Five)); + Assert.True (flagged.HasFlag (FlaggedEnum.Four | FlaggedEnum.Five)); + Assert.True ((flagged & (FlaggedEnum.Zero | FlaggedEnum.Five)) != 0); + Assert.True ((flagged & (FlaggedEnum.One | FlaggedEnum.Five)) != 0); + Assert.True ((flagged & (FlaggedEnum.Two | FlaggedEnum.Five)) != 0); + Assert.True ((flagged & (FlaggedEnum.Three | FlaggedEnum.Five)) != 0); + Assert.True ((flagged & (FlaggedEnum.Four | FlaggedEnum.Five)) != 0); + Assert.Equal (FlaggedEnum.Two | FlaggedEnum.Five, flagged); // As it is flagged shows as bitwise. + Assert.Equal ("Two, Five", flagged.ToString ()); + Assert.False (flagged == (FlaggedEnum.Zero | FlaggedEnum.Five)); + Assert.False (flagged == (FlaggedEnum.One | FlaggedEnum.Five)); + Assert.True (flagged == (FlaggedEnum.Two | FlaggedEnum.Five)); + Assert.True (flagged == (FlaggedEnum.Three | FlaggedEnum.Five)); + Assert.False (flagged == (FlaggedEnum.Four | FlaggedEnum.Five)); + } + + [Fact] + public void SimpleHighValueEnum_And_FlaggedHighValueEnum () + { + var simple = SimpleHighValueEnum.Three | SimpleHighValueEnum.Last; + + // This will not be well compared. + Assert.True (simple.HasFlag (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)); + Assert.True (simple.HasFlag (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)); + Assert.True (simple.HasFlag (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)); + Assert.True (simple.HasFlag (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)); + Assert.False (simple.HasFlag (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)); + Assert.True ((simple & (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)) != 0); + Assert.True ((simple & (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)) != 0); + Assert.True ((simple & (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)) != 0); + Assert.True ((simple & (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)) != 0); + Assert.True ((simple & (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)) != 0); + + // This will be well compared, because the SimpleHighValueEnum.Last have a high value. + Assert.Equal (1073741827, (int)simple); // As it is not flagged only shows as number. + Assert.Equal ("1073741827", simple.ToString ()); // As it is not flagged only shows as number. + Assert.False (simple == (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)); + Assert.False (simple == (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)); + Assert.False (simple == (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)); + Assert.True (simple == (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)); + Assert.False (simple == (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)); + + var flagged = FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last; + + // This will not be well compared. + Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)); + Assert.True (flagged.HasFlag (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)); + Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)); + Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)); + Assert.False (flagged.HasFlag (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)); + Assert.True ((flagged & (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)) != 0); + Assert.True ((flagged & (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)) != 0); + Assert.True ((flagged & (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)) != 0); + Assert.True ((flagged & (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)) != 0); + Assert.True ((flagged & (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)) != 0); + + // This will be well compared, because the SimpleHighValueEnum.Last have a high value. + Assert.Equal (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last, flagged); // As it is flagged shows as bitwise. + Assert.Equal ("Three, Last", flagged.ToString ()); // As it is flagged shows as bitwise. + Assert.False (flagged == (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)); + Assert.False (flagged == (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)); + Assert.False (flagged == (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)); + Assert.True (flagged == (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)); + Assert.False (flagged == (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)); + } + + [Fact] + public void Key_Enum_Ambiguity_Check () + { + var key = KeyCode.Y | KeyCode.CtrlMask; + + // This will not be well compared. + Assert.True (key.HasFlag (KeyCode.Q | KeyCode.CtrlMask)); + Assert.True ((key & (KeyCode.Q | KeyCode.CtrlMask)) != 0); + Assert.Equal (KeyCode.Y | KeyCode.CtrlMask, key); + Assert.Equal ("Y, CtrlMask", key.ToString ()); + + // This will be well compared, because the Key.CtrlMask have a high value. + Assert.False ((Key)key == Application.QuitKey); + switch (key) { + case KeyCode.Q | KeyCode.CtrlMask: + // Never goes here. + break; + case KeyCode.Y | KeyCode.CtrlMask: + Assert.True (key == (KeyCode.Y | KeyCode.CtrlMask)); + break; + default: + // Never goes here. + break; + } + } + + [Fact] + public void KeyEnum_ShouldHaveCorrectValues () + { + Assert.Equal (0, (int)KeyCode.Null); + Assert.Equal (8, (int)KeyCode.Backspace); + Assert.Equal (9, (int)KeyCode.Tab); + // Continue for other keys... + } + + [Fact] + public void Key_ToString () + { + var k = KeyCode.Y | KeyCode.CtrlMask; + Assert.Equal ("Y, CtrlMask", k.ToString ()); + + k = KeyCode.CtrlMask | KeyCode.Y; + Assert.Equal ("Y, CtrlMask", k.ToString ()); + + k = KeyCode.Space; + Assert.Equal ("Space", k.ToString ()); + + k = KeyCode.D; + Assert.Equal ("D", k.ToString ()); + + k = (KeyCode)'d'; + Assert.Equal ("d", ((char)k).ToString ()); + + k = KeyCode.D; + Assert.Equal ("D", k.ToString ()); + + // In a console this will always returns Key.D + k = KeyCode.D | KeyCode.ShiftMask; + Assert.Equal ("D, ShiftMask", k.ToString ()); + } +} \ No newline at end of file diff --git a/UnitTests/ConsoleDrivers/KeyTests.cs b/UnitTests/ConsoleDrivers/KeyTests.cs deleted file mode 100644 index b7ad45b28c..0000000000 --- a/UnitTests/ConsoleDrivers/KeyTests.cs +++ /dev/null @@ -1,348 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using Terminal.Gui; -using Xunit; - -namespace Terminal.Gui.InputTests { - public class KeyTests { - enum SimpleEnum { Zero, One, Two, Three, Four, Five } - - [Flags] - enum FlaggedEnum { Zero, One, Two, Three, Four, Five } - - enum SimpleHighValueEnum { Zero, One, Two, Three, Four, Last = 0x40000000 } - - [Flags] - enum FlaggedHighValueEnum { Zero, One, Two, Three, Four, Last = 0x40000000 } - - [Fact] - public void SimpleEnum_And_FlagedEnum () - { - var simple = SimpleEnum.Three | SimpleEnum.Five; - - // Nothing will not be well compared here. - Assert.True (simple.HasFlag (SimpleEnum.Zero | SimpleEnum.Five)); - Assert.True (simple.HasFlag (SimpleEnum.One | SimpleEnum.Five)); - Assert.True (simple.HasFlag (SimpleEnum.Two | SimpleEnum.Five)); - Assert.True (simple.HasFlag (SimpleEnum.Three | SimpleEnum.Five)); - Assert.True (simple.HasFlag (SimpleEnum.Four | SimpleEnum.Five)); - Assert.True ((simple & (SimpleEnum.Zero | SimpleEnum.Five)) != 0); - Assert.True ((simple & (SimpleEnum.One | SimpleEnum.Five)) != 0); - Assert.True ((simple & (SimpleEnum.Two | SimpleEnum.Five)) != 0); - Assert.True ((simple & (SimpleEnum.Three | SimpleEnum.Five)) != 0); - Assert.True ((simple & (SimpleEnum.Four | SimpleEnum.Five)) != 0); - Assert.Equal (7, (int)simple); // As it is not flagged only shows as number. - Assert.Equal ("7", simple.ToString ()); - Assert.False (simple == (SimpleEnum.Zero | SimpleEnum.Five)); - Assert.False (simple == (SimpleEnum.One | SimpleEnum.Five)); - Assert.True (simple == (SimpleEnum.Two | SimpleEnum.Five)); - Assert.True (simple == (SimpleEnum.Three | SimpleEnum.Five)); - Assert.False (simple == (SimpleEnum.Four | SimpleEnum.Five)); - - var flagged = FlaggedEnum.Three | FlaggedEnum.Five; - - // Nothing will not be well compared here. - Assert.True (flagged.HasFlag (FlaggedEnum.Zero | FlaggedEnum.Five)); - Assert.True (flagged.HasFlag (FlaggedEnum.One | FlaggedEnum.Five)); - Assert.True (flagged.HasFlag (FlaggedEnum.Two | FlaggedEnum.Five)); - Assert.True (flagged.HasFlag (FlaggedEnum.Three | FlaggedEnum.Five)); - Assert.True (flagged.HasFlag (FlaggedEnum.Four | FlaggedEnum.Five)); - Assert.True ((flagged & (FlaggedEnum.Zero | FlaggedEnum.Five)) != 0); - Assert.True ((flagged & (FlaggedEnum.One | FlaggedEnum.Five)) != 0); - Assert.True ((flagged & (FlaggedEnum.Two | FlaggedEnum.Five)) != 0); - Assert.True ((flagged & (FlaggedEnum.Three | FlaggedEnum.Five)) != 0); - Assert.True ((flagged & (FlaggedEnum.Four | FlaggedEnum.Five)) != 0); - Assert.Equal (FlaggedEnum.Two | FlaggedEnum.Five, flagged); // As it is flagged shows as bitwise. - Assert.Equal ("Two, Five", flagged.ToString ()); - Assert.False (flagged == (FlaggedEnum.Zero | FlaggedEnum.Five)); - Assert.False (flagged == (FlaggedEnum.One | FlaggedEnum.Five)); - Assert.True (flagged == (FlaggedEnum.Two | FlaggedEnum.Five)); - Assert.True (flagged == (FlaggedEnum.Three | FlaggedEnum.Five)); - Assert.False (flagged == (FlaggedEnum.Four | FlaggedEnum.Five)); - } - - [Fact] - public void SimpleHighValueEnum_And_FlaggedHighValueEnum () - { - var simple = SimpleHighValueEnum.Three | SimpleHighValueEnum.Last; - - // This will not be well compared. - Assert.True (simple.HasFlag (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)); - Assert.True (simple.HasFlag (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)); - Assert.True (simple.HasFlag (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)); - Assert.True (simple.HasFlag (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)); - Assert.False (simple.HasFlag (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)); - Assert.True ((simple & (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)) != 0); - Assert.True ((simple & (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)) != 0); - Assert.True ((simple & (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)) != 0); - Assert.True ((simple & (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)) != 0); - Assert.True ((simple & (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)) != 0); - - // This will be well compared, because the SimpleHighValueEnum.Last have a high value. - Assert.Equal (1073741827, (int)simple); // As it is not flagged only shows as number. - Assert.Equal ("1073741827", simple.ToString ()); // As it is not flagged only shows as number. - Assert.False (simple == (SimpleHighValueEnum.Zero | SimpleHighValueEnum.Last)); - Assert.False (simple == (SimpleHighValueEnum.One | SimpleHighValueEnum.Last)); - Assert.False (simple == (SimpleHighValueEnum.Two | SimpleHighValueEnum.Last)); - Assert.True (simple == (SimpleHighValueEnum.Three | SimpleHighValueEnum.Last)); - Assert.False (simple == (SimpleHighValueEnum.Four | SimpleHighValueEnum.Last)); - - var flagged = FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last; - - // This will not be well compared. - Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)); - Assert.True (flagged.HasFlag (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)); - Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)); - Assert.True (flagged.HasFlag (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)); - Assert.False (flagged.HasFlag (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)); - Assert.True ((flagged & (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)) != 0); - Assert.True ((flagged & (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)) != 0); - Assert.True ((flagged & (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)) != 0); - Assert.True ((flagged & (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)) != 0); - Assert.True ((flagged & (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)) != 0); - - // This will be well compared, because the SimpleHighValueEnum.Last have a high value. - Assert.Equal (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last, flagged); // As it is flagged shows as bitwise. - Assert.Equal ("Three, Last", flagged.ToString ()); // As it is flagged shows as bitwise. - Assert.False (flagged == (FlaggedHighValueEnum.Zero | FlaggedHighValueEnum.Last)); - Assert.False (flagged == (FlaggedHighValueEnum.One | FlaggedHighValueEnum.Last)); - Assert.False (flagged == (FlaggedHighValueEnum.Two | FlaggedHighValueEnum.Last)); - Assert.True (flagged == (FlaggedHighValueEnum.Three | FlaggedHighValueEnum.Last)); - Assert.False (flagged == (FlaggedHighValueEnum.Four | FlaggedHighValueEnum.Last)); - } - - [Fact] - public void Key_Enum_Ambiguity_Check () - { - var key = Key.Y | Key.CtrlMask; - - // This will not be well compared. - Assert.True (key.HasFlag (Key.Q | Key.CtrlMask)); - Assert.True ((key & (Key.Q | Key.CtrlMask)) != 0); - Assert.Equal (Key.Y | Key.CtrlMask, key); - Assert.Equal ("Y, CtrlMask", key.ToString ()); - - // This will be well compared, because the Key.CtrlMask have a high value. - Assert.False (key == Application.QuitKey); - switch (key) { - case Key.Q | Key.CtrlMask: - // Never goes here. - break; - case Key.Y | Key.CtrlMask: - Assert.True (key == (Key.Y | Key.CtrlMask)); - break; - default: - // Never goes here. - break; - } - } - - [Fact] - public void Key_ToString () - { - var k = Key.Y | Key.CtrlMask; - Assert.Equal ("Y, CtrlMask", k.ToString ()); - - k = Key.CtrlMask | Key.Y; - Assert.Equal ("Y, CtrlMask", k.ToString ()); - - k = Key.Space; - Assert.Equal ("Space", k.ToString ()); - - k = Key.Space | Key.D; - Assert.Equal ("d", k.ToString ()); - - k = (Key)'d'; - Assert.Equal ("d", k.ToString ()); - - k = Key.d; - Assert.Equal ("d", k.ToString ()); - - k = Key.D; - Assert.Equal ("D", k.ToString ()); - - // In a console this will always returns Key.D - k = Key.D | Key.ShiftMask; - Assert.Equal ("D, ShiftMask", k.ToString ()); - - // In a console this will always returns Key.D - k = Key.d | Key.ShiftMask; - Assert.Equal ("d, ShiftMask", k.ToString ()); - } - - private static object packetLock = new object (); - - /// - /// Sometimes when using remote tools EventKeyRecord sends 'virtual keystrokes'. - /// These are indicated with the wVirtualKeyCode of 231. When we see this code - /// then we need to look to the unicode character (UnicodeChar) instead of the key - /// when telling the rest of the framework what button was pressed. For full details - /// see: https://github.com/gui-cs/Terminal.Gui/issues/2008 - /// - [Theory, AutoInitShutdown] - [ClassData (typeof (PacketTest))] - public void TestVKPacket (uint unicodeCharacter, bool shift, bool alt, bool control, uint initialVirtualKey, uint initialScanCode, Key expectedRemapping, uint expectedVirtualKey, uint expectedScanCode) - { - lock (packetLock) { - Application._forceFakeConsole = true; - Application.Init (); - - var modifiers = new ConsoleModifiers (); - if (shift) modifiers |= ConsoleModifiers.Shift; - if (alt) modifiers |= ConsoleModifiers.Alt; - if (control) modifiers |= ConsoleModifiers.Control; - var mappedConsoleKey = ConsoleKeyMapping.GetConsoleKeyFromKey (unicodeCharacter, modifiers, out uint scanCode, out uint outputChar); - - if ((scanCode > 0 || mappedConsoleKey == 0) && mappedConsoleKey == initialVirtualKey) Assert.Equal (mappedConsoleKey, initialVirtualKey); - else Assert.Equal (mappedConsoleKey, outputChar < 0xff ? outputChar & 0xff | 0xff << 8 : outputChar); - Assert.Equal (scanCode, initialScanCode); - - var keyChar = ConsoleKeyMapping.GetKeyCharFromConsoleKey (mappedConsoleKey, modifiers, out uint consoleKey, out scanCode); - - //if (scanCode > 0 && consoleKey == keyChar && consoleKey > 48 && consoleKey > 57 && consoleKey < 65 && consoleKey > 91) { - if (scanCode > 0 && keyChar == 0 && consoleKey == mappedConsoleKey) Assert.Equal (0, (double)keyChar); - else Assert.Equal (keyChar, unicodeCharacter); - Assert.Equal (consoleKey, expectedVirtualKey); - Assert.Equal (scanCode, expectedScanCode); - - var top = Application.Top; - - top.KeyPressed += (s, e) => { - var after = ShortcutHelper.GetModifiersKey (e.KeyEvent); - Assert.Equal (expectedRemapping, after); - e.Handled = true; - Application.RequestStop (); - }; - - var iterations = -1; - - Application.Iteration += (s, a) => { - iterations++; - if (iterations == 0) Application.Driver.SendKeys ((char)mappedConsoleKey, ConsoleKey.Packet, shift, alt, control); - }; - Application.Run (); - Application.Shutdown (); - } - } - - public class PacketTest : IEnumerable, IEnumerable { - public IEnumerator GetEnumerator () - { - lock (packetLock) { - yield return new object [] { 'a', false, false, false, 'A', 30, Key.a, 'A', 30 }; - yield return new object [] { 'A', true, false, false, 'A', 30, Key.A | Key.ShiftMask, 'A', 30 }; - yield return new object [] { 'A', true, true, false, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask, 'A', 30 }; - yield return new object [] { 'A', true, true, true, 'A', 30, Key.A | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'A', 30 }; - yield return new object [] { 'z', false, false, false, 'Z', 44, Key.z, 'Z', 44 }; - yield return new object [] { 'Z', true, false, false, 'Z', 44, Key.Z | Key.ShiftMask, 'Z', 44 }; - yield return new object [] { 'Z', true, true, false, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask, 'Z', 44 }; - yield return new object [] { 'Z', true, true, true, 'Z', 44, Key.Z | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 'Z', 44 }; - yield return new object [] { '英', false, false, false, '\0', 0, (Key)'英', '\0', 0 }; - yield return new object [] { '英', true, false, false, '\0', 0, (Key)'英' | Key.ShiftMask, '\0', 0 }; - yield return new object [] { '英', true, true, false, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask, '\0', 0 }; - yield return new object [] { '英', true, true, true, '\0', 0, (Key)'英' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '\0', 0 }; - yield return new object [] { '+', false, false, false, 187, 26, (Key)'+', 187, 26 }; - yield return new object [] { '*', true, false, false, 187, 26, (Key)'*' | Key.ShiftMask, 187, 26 }; - yield return new object [] { '+', true, true, false, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask, 187, 26 }; - yield return new object [] { '+', true, true, true, 187, 26, (Key)'+' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 187, 26 }; - yield return new object [] { '1', false, false, false, '1', 2, Key.D1, '1', 2 }; - yield return new object [] { '!', true, false, false, '1', 2, (Key)'!' | Key.ShiftMask, '1', 2 }; - yield return new object [] { '1', true, true, false, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask, '1', 2 }; - yield return new object [] { '1', true, true, true, '1', 2, Key.D1 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '1', 2 }; - yield return new object [] { '1', false, true, true, '1', 2, Key.D1 | Key.AltMask | Key.CtrlMask, '1', 2 }; - yield return new object [] { '2', false, false, false, '2', 3, Key.D2, '2', 3 }; - yield return new object [] { '"', true, false, false, '2', 3, (Key)'"' | Key.ShiftMask, '2', 3 }; - yield return new object [] { '2', true, true, false, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask, '2', 3 }; - yield return new object [] { '2', true, true, true, '2', 3, Key.D2 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '2', 3 }; - yield return new object [] { '@', false, true, true, '2', 3, (Key)'@' | Key.AltMask | Key.CtrlMask, '2', 3 }; - yield return new object [] { '3', false, false, false, '3', 4, Key.D3, '3', 4 }; - yield return new object [] { '#', true, false, false, '3', 4, (Key)'#' | Key.ShiftMask, '3', 4 }; - yield return new object [] { '3', true, true, false, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask, '3', 4 }; - yield return new object [] { '3', true, true, true, '3', 4, Key.D3 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '3', 4 }; - yield return new object [] { '£', false, true, true, '3', 4, (Key)'£' | Key.AltMask | Key.CtrlMask, '3', 4 }; - yield return new object [] { '4', false, false, false, '4', 5, Key.D4, '4', 5 }; - yield return new object [] { '$', true, false, false, '4', 5, (Key)'$' | Key.ShiftMask, '4', 5 }; - yield return new object [] { '4', true, true, false, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask, '4', 5 }; - yield return new object [] { '4', true, true, true, '4', 5, Key.D4 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '4', 5 }; - yield return new object [] { '§', false, true, true, '4', 5, (Key)'§' | Key.AltMask | Key.CtrlMask, '4', 5 }; - yield return new object [] { '5', false, false, false, '5', 6, Key.D5, '5', 6 }; - yield return new object [] { '%', true, false, false, '5', 6, (Key)'%' | Key.ShiftMask, '5', 6 }; - yield return new object [] { '5', true, true, false, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask, '5', 6 }; - yield return new object [] { '5', true, true, true, '5', 6, Key.D5 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '5', 6 }; - yield return new object [] { '€', false, true, true, '5', 6, (Key)'€' | Key.AltMask | Key.CtrlMask, '5', 6 }; - yield return new object [] { '6', false, false, false, '6', 7, Key.D6, '6', 7 }; - yield return new object [] { '&', true, false, false, '6', 7, (Key)'&' | Key.ShiftMask, '6', 7 }; - yield return new object [] { '6', true, true, false, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask, '6', 7 }; - yield return new object [] { '6', true, true, true, '6', 7, Key.D6 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '6', 7 }; - yield return new object [] { '6', false, true, true, '6', 7, Key.D6 | Key.AltMask | Key.CtrlMask, '6', 7 }; - yield return new object [] { '7', false, false, false, '7', 8, Key.D7, '7', 8 }; - yield return new object [] { '/', true, false, false, '7', 8, (Key)'/' | Key.ShiftMask, '7', 8 }; - yield return new object [] { '7', true, true, false, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask, '7', 8 }; - yield return new object [] { '7', true, true, true, '7', 8, Key.D7 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '7', 8 }; - yield return new object [] { '{', false, true, true, '7', 8, (Key)'{' | Key.AltMask | Key.CtrlMask, '7', 8 }; - yield return new object [] { '8', false, false, false, '8', 9, Key.D8, '8', 9 }; - yield return new object [] { '(', true, false, false, '8', 9, (Key)'(' | Key.ShiftMask, '8', 9 }; - yield return new object [] { '8', true, true, false, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask, '8', 9 }; - yield return new object [] { '8', true, true, true, '8', 9, Key.D8 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '8', 9 }; - yield return new object [] { '[', false, true, true, '8', 9, (Key)'[' | Key.AltMask | Key.CtrlMask, '8', 9 }; - yield return new object [] { '9', false, false, false, '9', 10, Key.D9, '9', 10 }; - yield return new object [] { ')', true, false, false, '9', 10, (Key)')' | Key.ShiftMask, '9', 10 }; - yield return new object [] { '9', true, true, false, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask, '9', 10 }; - yield return new object [] { '9', true, true, true, '9', 10, Key.D9 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '9', 10 }; - yield return new object [] { ']', false, true, true, '9', 10, (Key)']' | Key.AltMask | Key.CtrlMask, '9', 10 }; - yield return new object [] { '0', false, false, false, '0', 11, Key.D0, '0', 11 }; - yield return new object [] { '=', true, false, false, '0', 11, (Key)'=' | Key.ShiftMask, '0', 11 }; - yield return new object [] { '0', true, true, false, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask, '0', 11 }; - yield return new object [] { '0', true, true, true, '0', 11, Key.D0 | Key.ShiftMask | Key.AltMask | Key.CtrlMask, '0', 11 }; - yield return new object [] { '}', false, true, true, '0', 11, (Key)'}' | Key.AltMask | Key.CtrlMask, '0', 11 }; - yield return new object [] { '\'', false, false, false, 219, 12, (Key)'\'', 219, 12 }; - yield return new object [] { '?', true, false, false, 219, 12, (Key)'?' | Key.ShiftMask, 219, 12 }; - yield return new object [] { '\'', true, true, false, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask, 219, 12 }; - yield return new object [] { '\'', true, true, true, 219, 12, (Key)'\'' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 219, 12 }; - yield return new object [] { '«', false, false, false, 221, 13, (Key)'«', 221, 13 }; - yield return new object [] { '»', true, false, false, 221, 13, (Key)'»' | Key.ShiftMask, 221, 13 }; - yield return new object [] { '«', true, true, false, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask, 221, 13 }; - yield return new object [] { '«', true, true, true, 221, 13, (Key)'«' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 221, 13 }; - yield return new object [] { 'á', false, false, false, 'á', 0, (Key)'á', 'A', 30 }; - yield return new object [] { 'Á', true, false, false, 'Á', 0, (Key)'Á' | Key.ShiftMask, 'A', 30 }; - yield return new object [] { 'à', false, false, false, 'à', 0, (Key)'à', 'A', 30 }; - yield return new object [] { 'À', true, false, false, 'À', 0, (Key)'À' | Key.ShiftMask, 'A', 30 }; - yield return new object [] { 'é', false, false, false, 'é', 0, (Key)'é', 'E', 18 }; - yield return new object [] { 'É', true, false, false, 'É', 0, (Key)'É' | Key.ShiftMask, 'E', 18 }; - yield return new object [] { 'è', false, false, false, 'è', 0, (Key)'è', 'E', 18 }; - yield return new object [] { 'È', true, false, false, 'È', 0, (Key)'È' | Key.ShiftMask, 'E', 18 }; - yield return new object [] { 'í', false, false, false, 'í', 0, (Key)'í', 'I', 23 }; - yield return new object [] { 'Í', true, false, false, 'Í', 0, (Key)'Í' | Key.ShiftMask, 'I', 23 }; - yield return new object [] { 'ì', false, false, false, 'ì', 0, (Key)'ì', 'I', 23 }; - yield return new object [] { 'Ì', true, false, false, 'Ì', 0, (Key)'Ì' | Key.ShiftMask, 'I', 23 }; - yield return new object [] { 'ó', false, false, false, 'ó', 0, (Key)'ó', 'O', 24 }; - yield return new object [] { 'Ó', true, false, false, 'Ó', 0, (Key)'Ó' | Key.ShiftMask, 'O', 24 }; - yield return new object [] { 'ò', false, false, false, 'Ó', 0, (Key)'ò', 'O', 24 }; - yield return new object [] { 'Ò', true, false, false, 'Ò', 0, (Key)'Ò' | Key.ShiftMask, 'O', 24 }; - yield return new object [] { 'ú', false, false, false, 'ú', 0, (Key)'ú', 'U', 22 }; - yield return new object [] { 'Ú', true, false, false, 'Ú', 0, (Key)'Ú' | Key.ShiftMask, 'U', 22 }; - yield return new object [] { 'ù', false, false, false, 'ù', 0, (Key)'ù', 'U', 22 }; - yield return new object [] { 'Ù', true, false, false, 'Ù', 0, (Key)'Ù' | Key.ShiftMask, 'U', 22 }; - yield return new object [] { 'ö', false, false, false, 'ó', 0, (Key)'ö', 'O', 24 }; - yield return new object [] { 'Ö', true, false, false, 'Ó', 0, (Key)'Ö' | Key.ShiftMask, 'O', 24 }; - yield return new object [] { '<', false, false, false, 226, 86, (Key)'<', 226, 86 }; - yield return new object [] { '>', true, false, false, 226, 86, (Key)'>' | Key.ShiftMask, 226, 86 }; - yield return new object [] { '<', true, true, false, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask, 226, 86 }; - yield return new object [] { '<', true, true, true, 226, 86, (Key)'<' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 226, 86 }; - yield return new object [] { 'ç', false, false, false, 192, 39, (Key)'ç', 192, 39 }; - yield return new object [] { 'Ç', true, false, false, 192, 39, (Key)'Ç' | Key.ShiftMask, 192, 39 }; - yield return new object [] { 'ç', true, true, false, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask, 192, 39 }; - yield return new object [] { 'ç', true, true, true, 192, 39, (Key)'ç' | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 192, 39 }; - yield return new object [] { '¨', false, true, true, 187, 26, (Key)'¨' | Key.AltMask | Key.CtrlMask, 187, 26 }; - yield return new object [] { (uint)Key.PageUp, false, false, false, 33, 73, Key.PageUp, 33, 73 }; - yield return new object [] { (uint)Key.PageUp, true, false, false, 33, 73, Key.PageUp | Key.ShiftMask, 33, 73 }; - yield return new object [] { (uint)Key.PageUp, true, true, false, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask, 33, 73 }; - yield return new object [] { (uint)Key.PageUp, true, true, true, 33, 73, Key.PageUp | Key.ShiftMask | Key.AltMask | Key.CtrlMask, 33, 73 }; - } - } - - IEnumerator IEnumerable.GetEnumerator () => GetEnumerator (); - } - } -} \ No newline at end of file diff --git a/UnitTests/Dialogs/DialogTests.cs b/UnitTests/Dialogs/DialogTests.cs index e01885ef9b..e39686e2b8 100644 --- a/UnitTests/Dialogs/DialogTests.cs +++ b/UnitTests/Dialogs/DialogTests.cs @@ -799,7 +799,7 @@ public void Dialog_Opened_From_Another_Dialog () Application.Iteration += (s, a) => { iterations++; if (iterations == 0) { - Assert.True (btn1.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (btn1.NewKeyDownEvent (new (KeyCode.Space))); } else if (iterations == 1) { expected = @$" ┌──────────────────────────────────────────────────────────────────┐ @@ -825,7 +825,7 @@ public void Dialog_Opened_From_Another_Dialog () └──────────────────────────────────────────────────────────────────┘"; TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (btn2.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (btn2.NewKeyDownEvent (new (KeyCode.Space))); } else if (iterations == 2) { TestHelpers.AssertDriverContentsWithFrameAre (@$" ┌──────────────────────────────────────────────────────────────────┐ @@ -850,11 +850,11 @@ public void Dialog_Opened_From_Another_Dialog () │ {CM.Glyphs.LeftBracket} Show Sub {CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Close {CM.Glyphs.RightBracket} │ └──────────────────────────────────────────────────────────────────┘", output); - Assert.True (Application.Current.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (Application.Current.NewKeyDownEvent (new (KeyCode.Enter))); } else if (iterations == 3) { TestHelpers.AssertDriverContentsWithFrameAre (expected, output); - Assert.True (btn3.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (btn3.NewKeyDownEvent (new (KeyCode.Space))); } else if (iterations == 4) { TestHelpers.AssertDriverContentsWithFrameAre ("", output); diff --git a/UnitTests/Drawing/ThicknessTests.cs b/UnitTests/Drawing/ThicknessTests.cs index 4eb66725d9..666ca24fc6 100644 --- a/UnitTests/Drawing/ThicknessTests.cs +++ b/UnitTests/Drawing/ThicknessTests.cs @@ -615,14 +615,12 @@ public void DrawTests_Ruler () [InlineData (0, 0, 10, 10, 9, 9, false)] // On opposite corner, in thickness [InlineData (0, 0, 10, 10, 5, 5, false)] // Inside the inner rectangle [InlineData (0, 0, 10, 10, -1, -1, false)] // Outside the outer rectangle - [InlineData (0, 0, 10, 10, 3, 3, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, 3, 3, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, 0, 0, false)] // On corner, in thickness [InlineData (0, 0, 0, 0, 9, 9, false)] // On opposite corner, in thickness [InlineData (0, 0, 0, 0, 5, 5, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, -1, -1, false)] // Outside the outer rectangle - [InlineData (0, 0, 0, 0, 3, 3, false)] // Inside the inner rectangle [InlineData (1, 1, 10, 10, 1, 1, false)] // On corner, in thickness [InlineData (1, 1, 10, 10, 10, 10, false)] // On opposite corner, in thickness @@ -647,14 +645,12 @@ public void TestContains_ZeroThickness (int x, int y, int width, int height, int [InlineData (0, 0, 10, 10, 9, 9, true)] // On opposite corner, in thickness [InlineData (0, 0, 10, 10, 5, 5, false)] // Inside the inner rectangle [InlineData (0, 0, 10, 10, -1, -1, false)] // Outside the outer rectangle - [InlineData (0, 0, 10, 10, 3, 3, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, 3, 3, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, 0, 0, false)] // On corner, in thickness [InlineData (0, 0, 0, 0, 9, 9, false)] // On opposite corner, in thickness [InlineData (0, 0, 0, 0, 5, 5, false)] // Inside the inner rectangle [InlineData (0, 0, 0, 0, -1, -1, false)] // Outside the outer rectangle - [InlineData (0, 0, 0, 0, 3, 3, false)] // Inside the inner rectangle [InlineData (1, 1, 10, 10, 1, 1, true)] // On corner, in thickness [InlineData (1, 1, 10, 10, 10, 10, true)] // On opposite corner, in thickness diff --git a/UnitTests/FileServices/FileDialogTests.cs b/UnitTests/FileServices/FileDialogTests.cs index 460dbef88b..14b89a2811 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -104,8 +104,13 @@ public void DoNotConfirmSelectionWhenFindFocused () var openIn = Path.Combine (Environment.CurrentDirectory, "zz"); Directory.CreateDirectory (openIn); dlg.Path = openIn + Path.DirectorySeparatorChar; - - Send ('f', ConsoleKey.F, false, false, true); +#if BROKE_IN_2927 + Send ('f', ConsoleKey.F, false, true, false); +#else + Application.OnKeyDown (new Key (KeyCode.Tab)); + Application.OnKeyDown (new Key (KeyCode.Tab)); + Application.OnKeyDown (new Key (KeyCode.Tab)); +#endif Assert.IsType (dlg.MostFocused); var tf = (TextField)dlg.MostFocused; @@ -176,7 +181,7 @@ public void PickDirectory_ArrowNavigation (bool openModeMixed, bool multiple) // Down to the directory Assert.True (dlg.Canceled); // Alt+O to open (enter would just navigate into the child dir) - Send ('o', ConsoleKey.O, false, true); + Send ('O', ConsoleKey.O, false, true); Assert.False (dlg.Canceled); AssertIsTheSubfolder (dlg.Path); @@ -210,7 +215,7 @@ public void MultiSelectDirectory_CannotToggleDotDot (bool acceptWithEnter) if (acceptWithEnter) { Send ('\n', ConsoleKey.Enter); } else { - Send ('o', ConsoleKey.O, false, true); + Send ('O', ConsoleKey.O, false, true); } Assert.False (dlg.Canceled); @@ -323,7 +328,7 @@ public void MultiSelectDirectory_CanToggleThenAccept (bool acceptWithEnter) if (acceptWithEnter) { Send ('\n', ConsoleKey.Enter); } else { - Send ('o', ConsoleKey.O, false, true); + Send ('O', ConsoleKey.O, false, true); } Assert.False (dlg.Canceled); @@ -398,7 +403,7 @@ public void TestDirectoryContents_Linux () │ │ │ │ │ │ -│{CM.Glyphs.LeftBracket} ►► {CM.Glyphs.RightBracket} Enter Search {CM.Glyphs.LeftBracket} OK {CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Cancel {CM.Glyphs.RightBracket} │ +│{CM.Glyphs.LeftBracket} ►► {CM.Glyphs.RightBracket} Enter Search {CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} OK {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Cancel {CM.Glyphs.RightBracket} │ └─────────────────────────────────────────────────────────────────────────┘ "; TestHelpers.AssertDriverContentsAre (expected, output, ignoreLeadingWhitespace: true); @@ -434,7 +439,7 @@ public void TestDirectoryContents_Windows () ││mybinary.exe│7.00 B │2001-01-01T11:44:42 │.exe ││ │ │ │ │ -│{CM.Glyphs.LeftBracket} ►► {CM.Glyphs.RightBracket} Enter Search {CM.Glyphs.LeftBracket} OK {CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Cancel {CM.Glyphs.RightBracket} │ +│{CM.Glyphs.LeftBracket} ►► {CM.Glyphs.RightBracket} Enter Search {CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} OK {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Cancel {CM.Glyphs.RightBracket} │ └─────────────────────────────────────────────────────────────────────────┘ "; TestHelpers.AssertDriverContentsAre (expected, output, ignoreLeadingWhitespace: true); diff --git a/UnitTests/Input/KeyBindingTests.cs b/UnitTests/Input/KeyBindingTests.cs new file mode 100644 index 0000000000..0dde97087a --- /dev/null +++ b/UnitTests/Input/KeyBindingTests.cs @@ -0,0 +1,288 @@ +using System; +using System.Collections.Generic; +using Xunit; +using Xunit.Abstractions; + +namespace Terminal.Gui.InputTests; + +public class KeyBindingTests { + readonly ITestOutputHelper _output; + + public KeyBindingTests (ITestOutputHelper output) + { + this._output = output; + } + + + [Fact] + public void Defaults () + { + var keyBindings = new KeyBindings (); + Assert.Throws (() => keyBindings.GetKeyFromCommands (Command.Accept)); + } + + [Fact] + public void Add_Single_Adds () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Default, resultCommands); + + keyBindings.Add (Key.B, Command.Default); + resultCommands = keyBindings.GetCommands (Key.B); + Assert.Contains (Command.Default, resultCommands); + } + + [Fact] + public void Add_Multiple_Adds () + { + var keyBindings = new KeyBindings (); + var commands = new Command [] { + Command.Right, + Command.Left + }; + + keyBindings.Add (Key.A, commands); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + + keyBindings.Add (Key.B, commands); + resultCommands = keyBindings.GetCommands (Key.B); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void Add_Empty_Throws () + { + var keyBindings = new KeyBindings (); + var commands = new List (); + Assert.Throws (() => keyBindings.Add (Key.A, commands.ToArray ())); + } + + // Add with scope does the right things + [Theory] + [InlineData (KeyBindingScope.Focused)] + [InlineData (KeyBindingScope.HotKey)] + [InlineData (KeyBindingScope.Application)] + public void Scope_Add_Adds (KeyBindingScope scope) + { + var keyBindings = new KeyBindings (); + var commands = new Command [] { + Command.Right, + Command.Left + }; + + keyBindings.Add (Key.A, scope, commands); + var binding = keyBindings.Get (Key.A); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + binding = keyBindings.Get (Key.A, scope); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Theory] + [InlineData (KeyBindingScope.Focused)] + [InlineData (KeyBindingScope.HotKey)] + [InlineData (KeyBindingScope.Application)] + public void Scope_Get_Filters (KeyBindingScope scope) + { + var keyBindings = new KeyBindings (); + var commands = new Command [] { + Command.Right, + Command.Left + }; + + keyBindings.Add (Key.A, scope, commands); + var binding = keyBindings.Get (Key.A); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + binding = keyBindings.Get (Key.A, scope); + Assert.Contains (Command.Right, binding.Commands); + Assert.Contains (Command.Left, binding.Commands); + + // negative test + binding = keyBindings.Get (Key.A, (KeyBindingScope)(int)-1); + Assert.Null (binding); + + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + // Clear + [Fact] + public void Clear_Clears () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + keyBindings.Add (Key.B, Command.Default); + keyBindings.Clear (); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Empty (resultCommands); + resultCommands = keyBindings.GetCommands (Key.B); + Assert.Empty (resultCommands); + } + + // GetCommands + [Fact] + public void GetCommands_Unknown_ReturnsEmpty () + { + var keyBindings = new KeyBindings (); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Empty (resultCommands); + } + + [Fact] + public void GetCommands_WithCommands_ReturnsCommands () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Default, resultCommands); + } + + [Fact] + public void GetCommands_WithMultipleCommands_ReturnsCommands () + { + var keyBindings = new KeyBindings (); + var commands = new Command [] { + Command.Right, + Command.Left + }; + keyBindings.Add (Key.A, commands); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + [Fact] + public void GetCommands_WithMultipleBindings_ReturnsCommands () + { + var keyBindings = new KeyBindings (); + var commands = new Command [] { + Command.Right, + Command.Left + }; + keyBindings.Add (Key.A, commands); + keyBindings.Add (Key.B, commands); + var resultCommands = keyBindings.GetCommands (Key.A); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + resultCommands = keyBindings.GetCommands (Key.B); + Assert.Contains (Command.Right, resultCommands); + Assert.Contains (Command.Left, resultCommands); + } + + // GetKeyFromCommands + [Fact] + public void GetKeyFromCommands_Unknown_Throws_InvalidOperationException () + { + var keyBindings = new KeyBindings (); + Assert.Throws (() => keyBindings.GetKeyFromCommands (Command.Accept)); + } + + [Fact] + public void GetKeyFromCommands_WithCommands_ReturnsKey () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + var resultKey = keyBindings.GetKeyFromCommands (Command.Default); + Assert.Equal (Key.A, resultKey); + } + + [Fact] + public void Replace_Key () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + keyBindings.Add (Key.B, Command.Default); + keyBindings.Add (Key.C, Command.Default); + keyBindings.Add (Key.D, Command.Default); + + keyBindings.Replace (Key.A, Key.E); + Assert.Empty (keyBindings.GetCommands (Key.A)); + Assert.Contains (Command.Default, keyBindings.GetCommands (Key.E)); + + keyBindings.Replace (Key.B, Key.E); + Assert.Empty (keyBindings.GetCommands (Key.B)); + Assert.Contains (Command.Default, keyBindings.GetCommands (Key.E)); + + keyBindings.Replace (Key.C, Key.E); + Assert.Empty (keyBindings.GetCommands (Key.C)); + Assert.Contains (Command.Default, keyBindings.GetCommands (Key.E)); + + keyBindings.Replace (Key.D, Key.E); + Assert.Empty (keyBindings.GetCommands (Key.D)); + Assert.Contains (Command.Default, keyBindings.GetCommands (Key.E)); + } + + // TryGet + [Fact] + public void TryGet_Unknown_ReturnsFalse () + { + var keyBindings = new KeyBindings (); + var result = keyBindings.TryGet (Key.A, out var _); + Assert.False (result); + } + + [Fact] + public void TryGet_WithCommands_ReturnsTrue () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Default); + var result = keyBindings.TryGet (Key.A, out var bindings); + Assert.True (result); + Assert.Contains (Command.Default, bindings.Commands); + } + + + [Fact] + public void GetKeyFromCommands_OneCommand () + { + var keyBindings = new KeyBindings (); + keyBindings.Add (Key.A, Command.Right); + + var key = keyBindings.GetKeyFromCommands (Command.Right); + Assert.Equal (Key.A, key); + + // Negative case + Assert.Throws (() => key = keyBindings.GetKeyFromCommands (Command.Left)); + } + + + [Fact] + public void GetKeyFromCommands_MultipleCommands () + { + var keyBindings = new KeyBindings (); + var commands1 = new Command [] { + Command.Right, + Command.Left + }; + keyBindings.Add (Key.A, commands1); + + var commands2 = new Command [] { + Command.LineUp, + Command.LineDown + }; + keyBindings.Add (Key.B, commands2); + + var key = keyBindings.GetKeyFromCommands (commands1); + Assert.Equal (Key.A, key); + + key = keyBindings.GetKeyFromCommands (commands2); + Assert.Equal (Key.B, key); + + // Negative case + Assert.Throws (() => key = keyBindings.GetKeyFromCommands (Command.EndOfLine)); + } +} \ No newline at end of file diff --git a/UnitTests/Input/KeyTests.cs b/UnitTests/Input/KeyTests.cs new file mode 100644 index 0000000000..8e2f3531b4 --- /dev/null +++ b/UnitTests/Input/KeyTests.cs @@ -0,0 +1,325 @@ +using System; +using System.Text; +using Xunit; +using Xunit.Abstractions; + +namespace Terminal.Gui.InputTests; + +public class KeyTests { + readonly ITestOutputHelper _output; + + public KeyTests (ITestOutputHelper output) => _output = output; + + [Fact] + public void Constructor_Default_ShouldSetKeyToNull () + { + var eventArgs = new Key (); + Assert.Equal (KeyCode.Null, eventArgs.KeyCode); + } + + [Theory] + [InlineData (KeyCode.Enter)] + [InlineData (KeyCode.Esc)] + [InlineData (KeyCode.A)] + public void Constructor_WithKey_ShouldSetCorrectKey (KeyCode key) + { + var eventArgs = new Key (key); + Assert.Equal (key, eventArgs.KeyCode); + } + + [Fact] + public void HandledProperty_ShouldBeFalseByDefault () + { + var eventArgs = new Key (); + Assert.False (eventArgs.Handled); + } + + [Theory] + [InlineData (KeyCode.Enter, KeyCode.Enter)] + [InlineData (KeyCode.Esc, KeyCode.Esc)] + [InlineData (KeyCode.A, (KeyCode)'a')] + [InlineData (KeyCode.A | KeyCode.ShiftMask, KeyCode.A | KeyCode.ShiftMask)] + [InlineData (KeyCode.Z, (KeyCode)'z')] + [InlineData (KeyCode.Space, KeyCode.Space)] + public void Cast_KeyCode_To_Key (KeyCode cdk, Key expected) + { + // explicit + var key = (Key)cdk; + Assert.Equal (expected.ToString (), key.ToString ()); + + // implicit + key = cdk; + Assert.Equal (expected.ToString (), key.ToString ()); + } + + [Theory] + [InlineData ((KeyCode)'a', true)] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, true)] + [InlineData (KeyCode.A, true)] + [InlineData (KeyCode.A | KeyCode.ShiftMask, true)] + [InlineData (KeyCode.F, true)] + [InlineData (KeyCode.F | KeyCode.ShiftMask, true)] + // these have alt or ctrl modifiers or are not a..z + [InlineData (KeyCode.A | KeyCode.CtrlMask, false)] + [InlineData (KeyCode.A | KeyCode.AltMask, false)] + [InlineData (KeyCode.D0, false)] + [InlineData (KeyCode.Esc, false)] + [InlineData (KeyCode.Tab, false)] + public void IsKeyCodeAtoZ (KeyCode key, bool expected) + { + var eventArgs = new Key (key); + Assert.Equal (expected, eventArgs.IsKeyCodeAtoZ); + } + + [Theory] + [InlineData ((KeyCode)'❿', '❿')] + [InlineData ((KeyCode)'☑', '☑')] + [InlineData ((KeyCode)'英', '英')] + [InlineData ((KeyCode)'{', '{')] + [InlineData ((KeyCode)'\'', '\'')] + [InlineData ((KeyCode)'\r', '\r')] + [InlineData ((KeyCode)'ó', 'ó')] + [InlineData ((KeyCode)'ó' | KeyCode.ShiftMask, 'ó')] + [InlineData ((KeyCode)'Ó', 'Ó')] + [InlineData ((KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, '\0')] + [InlineData ((KeyCode)'a', 97)] // 97 or Key.Space | Key.A + [InlineData ((KeyCode)'A', 97)] // 65 or equivalent to Key.A, but A-Z are mapped to lower case by drivers + //[InlineData (Key.A, 97)] // 65 equivalent to (Key)'A', but A-Z are mapped to lower case by drivers + [InlineData (KeyCode.ShiftMask | KeyCode.A, 65)] + [InlineData (KeyCode.CtrlMask | KeyCode.A, '\0')] + [InlineData (KeyCode.AltMask | KeyCode.A, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.A, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.A, '\0')] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.A, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.A, '\0')] + [InlineData ((KeyCode)'z', 'z')] + [InlineData ((KeyCode)'Z', 'z')] + [InlineData (KeyCode.ShiftMask | KeyCode.Z, 'Z')] + [InlineData ((KeyCode)'1', '1')] + [InlineData (KeyCode.ShiftMask | KeyCode.D1, '1')] + [InlineData (KeyCode.CtrlMask | KeyCode.D1, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.D1, '\0')] + [InlineData (KeyCode.F1, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.F1, '\0')] + [InlineData (KeyCode.CtrlMask | KeyCode.F1, '\0')] + [InlineData (KeyCode.Enter, '\n')] + [InlineData (KeyCode.Tab, '\t')] + [InlineData (KeyCode.Esc, 0x1b)] + [InlineData (KeyCode.Space, ' ')] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Enter, '\0')] + [InlineData (KeyCode.Null, '\0')] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Null, '\0')] + [InlineData (KeyCode.CharMask, '\0')] + [InlineData (KeyCode.SpecialMask, '\0')] + public void AsRune_ShouldReturnCorrectIntValue (KeyCode key, Rune expected) + { + var eventArgs = new Key (key); + Assert.Equal (expected, eventArgs.AsRune); + } + + [Theory] + [InlineData (KeyCode.AltMask, true)] + [InlineData (KeyCode.A, false)] + public void IsAlt_ShouldReturnCorrectValue (KeyCode key, bool expected) + { + var eventArgs = new Key (key); + Assert.Equal (expected, eventArgs.IsAlt); + } + + [Fact] + public void WithShift_ShouldReturnCorrectValue () + { + var a = new Key (KeyCode.A); + Assert.Equal (KeyCode.A | KeyCode.ShiftMask, a.WithShift); + + var CAD = Key.Delete.WithCtrl.WithAlt; + Assert.Equal (KeyCode.Delete | KeyCode.CtrlMask | KeyCode.AltMask, CAD); + } + + [Fact] + public void NoShift_ShouldReturnCorrectValue () + { + var CAD = Key.Delete.WithCtrl.WithAlt; + Assert.Equal (KeyCode.Delete | KeyCode.CtrlMask | KeyCode.AltMask, CAD); + + Assert.Equal (KeyCode.Delete | KeyCode.AltMask, CAD.NoCtrl); + + var a = new Key (KeyCode.A).WithCtrl.WithAlt.WithShift; + Assert.Equal (KeyCode.A, a.NoCtrl.NoShift.NoAlt); + Assert.Equal (KeyCode.A, a.NoAlt.NoShift.NoCtrl); + Assert.Equal (KeyCode.A, a.NoAlt.NoShift.NoCtrl.NoCtrl.NoAlt.NoShift); + + Assert.Equal (Key.Delete, Key.Delete.WithCtrl.NoCtrl); + + Assert.Equal ((KeyCode)Key.Delete | KeyCode.CtrlMask, Key.Delete.NoCtrl.WithCtrl); + } + + [Fact] + public void Standard_Keys_Should_Equal_KeyCode () + { + Assert.Equal (KeyCode.A, Key.A); + Assert.Equal (KeyCode.Delete, Key.Delete); + } + + // TODO: Create equality operator for KeyCode + //Assert.Equal (KeyCode.Delete, Key.Delete); + + // Similar tests for IsShift and IsCtrl + [Fact] + public void ToString_ShouldReturnReadableString () + { + var eventArgs = new Key (KeyCode.CtrlMask | KeyCode.A); + Assert.Equal ("Ctrl+A", eventArgs.ToString ()); + } + + [Theory] + [InlineData (KeyCode.CtrlMask | KeyCode.A, '+', "Ctrl+A")] + [InlineData (KeyCode.AltMask | KeyCode.B, '-', "Alt-B")] + public void ToStringWithSeparator_ShouldReturnFormattedString (KeyCode key, char separator, string expected) => Assert.Equal (expected, Key.ToString (key, (Rune)separator)); + + [Theory] + [InlineData ((KeyCode)'☑', "☑")] + //[InlineData ((ConsoleDriverKey)'英', "英")] + //[InlineData ((ConsoleDriverKey)'{', "{")] + [InlineData ((KeyCode)'\'', "\'")] + [InlineData ((KeyCode)'ó', "ó")] + [InlineData ((KeyCode)'ó' | KeyCode.ShiftMask, "Shift+ó")] // is this right??? + [InlineData ((KeyCode)'Ó', "Ó")] + [InlineData ((KeyCode)'ç' | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask, "Ctrl+Alt+Shift+ç")] + [InlineData ((KeyCode)'a', "a")] // 97 or Key.Space | Key.A + [InlineData ((KeyCode)'A', "a")] // 65 or equivalent to Key.A, but A-Z are mapped to lower case by drivers + [InlineData (KeyCode.ShiftMask | KeyCode.A, "A")] + [InlineData ((KeyCode)'a' | KeyCode.ShiftMask, "A")] + [InlineData (KeyCode.CtrlMask | KeyCode.A, "Ctrl+A")] + [InlineData (KeyCode.AltMask | KeyCode.A, "Alt+A")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.A, "Ctrl+Shift+A")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.A, "Alt+Shift+A")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.A, "Ctrl+Alt+A")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.A, "Ctrl+Alt+Shift+A")] + [InlineData (KeyCode.ShiftMask | KeyCode.Z, "Z")] + [InlineData (KeyCode.CtrlMask | KeyCode.Z, "Ctrl+Z")] + [InlineData (KeyCode.AltMask | KeyCode.Z, "Alt+Z")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Z, "Ctrl+Shift+Z")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Z, "Alt+Shift+Z")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Z, "Ctrl+Alt+Z")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Z, "Ctrl+Alt+Shift+Z")] + [InlineData ((KeyCode)'1', "1")] + [InlineData (KeyCode.ShiftMask | KeyCode.D1, "Shift+1")] + [InlineData (KeyCode.CtrlMask | KeyCode.D1, "Ctrl+1")] + [InlineData (KeyCode.AltMask | KeyCode.D1, "Alt+1")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.D1, "Ctrl+Shift+1")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.D1, "Alt+Shift+1")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.D1, "Ctrl+Alt+1")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.D1, "Ctrl+Alt+Shift+1")] + [InlineData (KeyCode.F1, "F1")] + [InlineData (KeyCode.ShiftMask | KeyCode.F1, "Shift+F1")] + [InlineData (KeyCode.CtrlMask | KeyCode.F1, "Ctrl+F1")] + [InlineData (KeyCode.AltMask | KeyCode.F1, "Alt+F1")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.F1, "Ctrl+Shift+F1")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.F1, "Alt+Shift+F1")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.F1, "Ctrl+Alt+F1")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.F1, "Ctrl+Alt+Shift+F1")] + [InlineData (KeyCode.Enter, "Enter")] + [InlineData (KeyCode.ShiftMask | KeyCode.Enter, "Shift+Enter")] + [InlineData (KeyCode.CtrlMask | KeyCode.Enter, "Ctrl+Enter")] + [InlineData (KeyCode.AltMask | KeyCode.Enter, "Alt+Enter")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Enter, "Ctrl+Shift+Enter")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Enter, "Alt+Shift+Enter")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Enter, "Ctrl+Alt+Enter")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Enter, "Ctrl+Alt+Shift+Enter")] + [InlineData (KeyCode.Delete, "Delete")] + [InlineData (KeyCode.ShiftMask | KeyCode.Delete, "Shift+Delete")] + [InlineData (KeyCode.CtrlMask | KeyCode.Delete, "Ctrl+Delete")] + [InlineData (KeyCode.AltMask | KeyCode.Delete, "Alt+Delete")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Delete, "Ctrl+Shift+Delete")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Delete, "Alt+Shift+Delete")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Delete, "Ctrl+Alt+Delete")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Delete, "Ctrl+Alt+Shift+Delete")] + [InlineData (KeyCode.CursorUp, "CursorUp")] + [InlineData (KeyCode.ShiftMask | KeyCode.CursorUp, "Shift+CursorUp")] + [InlineData (KeyCode.CtrlMask | KeyCode.CursorUp, "Ctrl+CursorUp")] + [InlineData (KeyCode.AltMask | KeyCode.CursorUp, "Alt+CursorUp")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.CursorUp, "Ctrl+Shift+CursorUp")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CursorUp, "Alt+Shift+CursorUp")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.CursorUp, "Ctrl+Alt+CursorUp")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.CursorUp, "Ctrl+Alt+Shift+CursorUp")] + [InlineData (KeyCode.Unknown, "Unknown")] + [InlineData (KeyCode.ShiftMask | KeyCode.Unknown, "Shift+Unknown")] + [InlineData (KeyCode.CtrlMask | KeyCode.Unknown, "Ctrl+Unknown")] + [InlineData (KeyCode.AltMask | KeyCode.Unknown, "Alt+Unknown")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Unknown, "Ctrl+Shift+Unknown")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Unknown, "Alt+Shift+Unknown")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Unknown, "Ctrl+Alt+Unknown")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Unknown, "Ctrl+Alt+Shift+Unknown")] + [InlineData (KeyCode.Null, "")] + [InlineData (KeyCode.ShiftMask | KeyCode.Null, "Shift")] + [InlineData (KeyCode.CtrlMask | KeyCode.Null, "Ctrl")] + [InlineData (KeyCode.AltMask | KeyCode.Null, "Alt")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.Null, "Ctrl+Shift")] + [InlineData (KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.Null, "Alt+Shift")] + [InlineData (KeyCode.AltMask | KeyCode.CtrlMask | KeyCode.Null, "Ctrl+Alt")] + [InlineData (KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Null, "Ctrl+Alt+Shift")] + [InlineData (KeyCode.CharMask, "CharMask")] + [InlineData (KeyCode.SpecialMask, "Ctrl+Alt+Shift")] + public void ToString_ShouldReturnFormattedString (KeyCode key, string expected) => Assert.Equal (expected, Key.ToString (key)); + + // TryParse + [Theory] + [InlineData ("a", KeyCode.A)] + [InlineData ("Ctrl+A", KeyCode.A | KeyCode.CtrlMask)] + [InlineData ("Alt+A", KeyCode.A | KeyCode.AltMask)] + [InlineData ("Shift+A", KeyCode.A | KeyCode.ShiftMask)] + [InlineData ("A", KeyCode.A | KeyCode.ShiftMask)] + [InlineData ("â", (KeyCode)'â')] + [InlineData ("Shift+â", (KeyCode)'â' | KeyCode.ShiftMask)] + [InlineData ("Shift+Â", (KeyCode)'Â' | KeyCode.ShiftMask)] + [InlineData ("Ctrl+Shift+CursorUp", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.CursorUp)] + [InlineData ("Ctrl+Alt+Shift+CursorUp", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.CursorUp)] + [InlineData ("ctrl+alt+shift+cursorup", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.CursorUp)] + [InlineData ("CTRL+ALT+SHIFT+CURSORUP", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.CursorUp)] + [InlineData ("Ctrl+Alt+Shift+Delete", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Delete)] + [InlineData ("Ctrl+Alt+Shift+Enter", KeyCode.ShiftMask | KeyCode.CtrlMask | KeyCode.AltMask | KeyCode.Enter)] + [InlineData ("Tab", KeyCode.Tab)] + [InlineData ("Shift+Tab", KeyCode.Tab | KeyCode.ShiftMask)] + [InlineData ("Ctrl+Tab", KeyCode.Tab | KeyCode.CtrlMask)] + [InlineData ("Alt+Tab", KeyCode.Tab | KeyCode.AltMask)] + [InlineData ("Ctrl+Shift+Tab", KeyCode.Tab | KeyCode.ShiftMask | KeyCode.CtrlMask)] + [InlineData ("Ctrl+Alt+Tab", KeyCode.Tab | KeyCode.AltMask | KeyCode.CtrlMask)] + [InlineData ("", KeyCode.Null)] + [InlineData (" ", KeyCode.Space)] + [InlineData ("Shift+ ", KeyCode.Space | KeyCode.ShiftMask)] + [InlineData ("Ctrl+ ", KeyCode.Space | KeyCode.CtrlMask)] + [InlineData ("Alt+ ", KeyCode.Space | KeyCode.AltMask)] + [InlineData ("F1", KeyCode.F1)] + [InlineData ("0", KeyCode.D0)] + [InlineData ("9", KeyCode.D9)] + [InlineData ("D0", KeyCode.D0)] + [InlineData ("65", KeyCode.A | KeyCode.ShiftMask)] + [InlineData ("97", KeyCode.A)] + [InlineData ("Shift", KeyCode.ShiftKey)] + [InlineData ("Ctrl", KeyCode.CtrlKey)] + [InlineData ("Ctrl-A", KeyCode.A | KeyCode.CtrlMask)] + [InlineData ("Alt-A", KeyCode.A | KeyCode.AltMask)] + [InlineData ("A-Ctrl", KeyCode.A | KeyCode.CtrlMask)] + [InlineData ("Alt-A-Ctrl", KeyCode.A | KeyCode.CtrlMask | KeyCode.AltMask)] + public void TryParse_ShouldReturnTrue_WhenValidKey (string keyString, Key expected) + { + Key key; + Assert.True (Key.TryParse (keyString, out key)); + Assert.Equal (((Key)expected).ToString (), key.ToString ()); + } + + [Theory] + [InlineData ("aa")] + [InlineData ("-1")] + [InlineData ("Crtl-A")] + [InlineData ("Ctrl=A")] + [InlineData ("Crtl")] + [InlineData ("99a")] + [InlineData ("a99")] + [InlineData ("#99")] + [InlineData ("x99")] + [InlineData ("0x99")] + [InlineData ("Ctrl-Ctrl")] + public void TryParse_ShouldReturnFalse_On_InvalidKey (string keyString) => Assert.False (Key.TryParse (keyString, out var _)); +} \ No newline at end of file diff --git a/UnitTests/Input/ResponderTests.cs b/UnitTests/Input/ResponderTests.cs index 304d771c83..005137d112 100644 --- a/UnitTests/Input/ResponderTests.cs +++ b/UnitTests/Input/ResponderTests.cs @@ -3,118 +3,130 @@ // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; -namespace Terminal.Gui.InputTests { - public class ResponderTests { - [Fact, TestRespondersDisposed] - public void New_Initializes () - { - var r = new Responder (); - Assert.NotNull (r); - Assert.Equal ("Terminal.Gui.Responder", r.ToString ()); - Assert.False (r.CanFocus); - Assert.False (r.HasFocus); - Assert.True (r.Enabled); - Assert.True (r.Visible); - r.Dispose (); - } +namespace Terminal.Gui.InputTests; + +public class ResponderTests { + [Fact] [TestRespondersDisposed] + public void New_Initializes () + { + var r = new Responder (); + Assert.NotNull (r); + Assert.Equal ("Terminal.Gui.Responder", r.ToString ()); + Assert.False (r.CanFocus); + Assert.False (r.HasFocus); + Assert.True (r.Enabled); + Assert.True (r.Visible); + r.Dispose (); + } - [Fact, TestRespondersDisposed] - public void New_Methods_Return_False () - { - var r = new Responder (); - - Assert.False (r.ProcessKey (new KeyEvent () { Key = Key.Unknown })); - Assert.False (r.ProcessHotKey (new KeyEvent () { Key = Key.Unknown })); - Assert.False (r.ProcessColdKey (new KeyEvent () { Key = Key.Unknown })); - Assert.False (r.OnKeyDown (new KeyEvent () { Key = Key.Unknown })); - Assert.False (r.OnKeyUp (new KeyEvent () { Key = Key.Unknown })); - Assert.False (r.MouseEvent (new MouseEvent () { Flags = MouseFlags.AllEvents })); - Assert.False (r.OnMouseEnter (new MouseEvent () { Flags = MouseFlags.AllEvents })); - Assert.False (r.OnMouseLeave (new MouseEvent () { Flags = MouseFlags.AllEvents })); - - var v = new View (); - Assert.False (r.OnEnter (v)); - v.Dispose (); - - v = new View (); - Assert.False (r.OnLeave (v)); - v.Dispose (); - - r.Dispose (); - } + [Fact] [TestRespondersDisposed] + public void New_Methods_Return_False () + { + var r = new View (); - // Generic lifetime (IDisposable) tests - [Fact, TestRespondersDisposed] - public void Dispose_Works () - { - - var r = new Responder (); + //Assert.False (r.OnKeyDown (new KeyEventArgs () { Key = Key.Unknown })); + Assert.False (r.OnKeyDown (new Key () { KeyCode = KeyCode.Unknown })); + Assert.False (r.OnKeyUp (new Key () { KeyCode = KeyCode.Unknown })); + Assert.False (r.MouseEvent (new MouseEvent () { Flags = MouseFlags.AllEvents })); + Assert.False (r.OnMouseEnter (new MouseEvent () { Flags = MouseFlags.AllEvents })); + Assert.False (r.OnMouseLeave (new MouseEvent () { Flags = MouseFlags.AllEvents })); + + var v = new View (); + Assert.False (r.OnEnter (v)); + v.Dispose (); + + v = new View (); + Assert.False (r.OnLeave (v)); + v.Dispose (); + + r.Dispose (); + } + + [Fact] + public void KeyPressed_Handled_True_Cancels_KeyPress () + { + var r = new View (); + var args = new Key () { KeyCode = KeyCode.Unknown }; + + Assert.False (r.OnKeyDown (args)); + Assert.False (args.Handled); + + r.KeyDown += (s, a) => a.Handled = true; + Assert.True (r.OnKeyDown (args)); + Assert.True (args.Handled); + + r.Dispose (); + } + + // Generic lifetime (IDisposable) tests + [Fact] [TestRespondersDisposed] + public void Dispose_Works () + { + + var r = new Responder (); #if DEBUG_IDISPOSABLE - Assert.Single (Responder.Instances); + Assert.Single (Responder.Instances); #endif - r.Dispose (); + r.Dispose (); #if DEBUG_IDISPOSABLE - Assert.Empty (Responder.Instances); + Assert.Empty (Responder.Instances); #endif - } + } - public class DerivedView : View { - public DerivedView () - { - } + public class DerivedView : View { + public DerivedView () { } - public override bool OnKeyDown (KeyEvent keyEvent) - { - return true; - } + public override bool OnKeyDown (Key keyEvent) + { + return true; } + } - [Fact, TestRespondersDisposed] - public void IsOverridden_False_IfNotOverridden () - { - // MouseEvent IS defined on Responder but NOT overridden - Assert.False (Responder.IsOverridden (new Responder () { }, "MouseEvent")); + [Fact] [TestRespondersDisposed] + public void IsOverridden_False_IfNotOverridden () + { + // MouseEvent IS defined on Responder but NOT overridden + Assert.False (Responder.IsOverridden (new Responder () { }, "MouseEvent")); - // MouseEvent is defined on Responder and NOT overrident on View - Assert.False (Responder.IsOverridden (new View () { Text = "View does not override MouseEvent" }, "MouseEvent")); - Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); + // MouseEvent is defined on Responder and NOT overrident on View + Assert.False (Responder.IsOverridden (new View () { Text = "View does not override MouseEvent" }, "MouseEvent")); + Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); - // MouseEvent is NOT defined on DerivedView - Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); + // MouseEvent is NOT defined on DerivedView + Assert.False (Responder.IsOverridden (new DerivedView () { Text = "DerivedView does not override MouseEvent" }, "MouseEvent")); - // OnKeyDown is defined on View and NOT overrident on Button - Assert.False (Responder.IsOverridden (new Button () { Text = "Button does not override OnKeyDown" }, "OnKeyDown")); + // OnKeyDown is defined on View and NOT overrident on Button + Assert.False (Responder.IsOverridden (new Button () { Text = "Button does not override OnKeyDown" }, "OnKeyDown")); #if DEBUG_IDISPOSABLE - // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above. - Responder.Instances.Clear (); - Assert.Empty (Responder.Instances); + // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above. + Responder.Instances.Clear (); + Assert.Empty (Responder.Instances); #endif - } + } - [Fact, TestRespondersDisposed] - public void IsOverridden_True_IfOverridden () - { - // MouseEvent is defined on Responder IS overriden on ScrollBarView (but not View) - Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); + [Fact] [TestRespondersDisposed] + public void IsOverridden_True_IfOverridden () + { + // MouseEvent is defined on Responder IS overriden on ScrollBarView (but not View) + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); - // OnKeyDown is defined on View - Assert.True (Responder.IsOverridden (new View () { Text = "View overrides OnKeyDown" }, "OnKeyDown")); + //// OnKeyDown is defined on View + //Assert.True (Responder.IsOverridden (new View () { Text = "View overrides OnKeyDown" }, "OnKeyDown")); - // OnKeyDown is defined on DerivedView - Assert.True (Responder.IsOverridden (new DerivedView () { Text = "DerivedView overrides OnKeyDown" }, "OnKeyDown")); + //// OnKeyDown is defined on DerivedView + //Assert.True (Responder.IsOverridden (new DerivedView () { Text = "DerivedView overrides OnKeyDown" }, "OnKeyDown")); - // ScrollBarView overrides both MouseEvent (from Responder) and Redraw (from View) - Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); - Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides OnDrawContent" }, "OnDrawContent")); + // ScrollBarView overrides both MouseEvent (from Responder) and Redraw (from View) + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides MouseEvent" }, "MouseEvent")); + Assert.True (Responder.IsOverridden (new ScrollBarView () { Text = "ScrollBarView overrides OnDrawContent" }, "OnDrawContent")); - Assert.True (Responder.IsOverridden (new Button () { Text = "Button overrides MouseEvent" }, "MouseEvent")); + Assert.True (Responder.IsOverridden (new Button () { Text = "Button overrides MouseEvent" }, "MouseEvent")); #if DEBUG_IDISPOSABLE - // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above. - Responder.Instances.Clear (); - Assert.Empty (Responder.Instances); + // HACK: Force clean up of Responders to avoid having to Dispose all the Views created above. + Responder.Instances.Clear (); + Assert.Empty (Responder.Instances); #endif - } } -} +} \ No newline at end of file diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 6f09942a51..b6a34c5810 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -12,6 +12,7 @@ using Microsoft.VisualStudio.TestPlatform.Utilities; using Xunit.Sdk; using System.Globalization; +using System.IO; namespace Terminal.Gui; // This class enables test functions annotated with the [AutoInitShutdown] attribute to @@ -425,4 +426,98 @@ private static string ReplaceNewLinesToPlatformSpecific (string toReplace) return replaced; } -} + + /// + /// Gets a list of instances of all classes derived from View. + /// + /// List of View objects + public static List GetAllViews () + { + return typeof (View).Assembly.GetTypes () + .Where (type => type.IsClass && !type.IsAbstract && type.IsPublic && type.IsSubclassOf (typeof (View))) + .Select (type => GetTypeInitializer (type, type.GetConstructor (Array.Empty ()))).ToList (); + } + + private static View GetTypeInitializer (Type type, ConstructorInfo ctor) + { + View viewType = null; + + if (type.IsGenericType && type.IsTypeDefinition) { + List gTypes = new List (); + + foreach (var args in type.GetGenericArguments ()) { + gTypes.Add (typeof (object)); + } + type = type.MakeGenericType (gTypes.ToArray ()); + + Assert.IsType (type, (View)Activator.CreateInstance (type)); + + } else { + ParameterInfo [] paramsInfo = ctor.GetParameters (); + Type paramType; + List pTypes = new List (); + + if (type.IsGenericType) { + foreach (var args in type.GetGenericArguments ()) { + paramType = args.GetType (); + if (args.Name == "T") { + pTypes.Add (typeof (object)); + } else { + AddArguments (paramType, pTypes); + } + } + } + + foreach (var p in paramsInfo) { + paramType = p.ParameterType; + if (p.HasDefaultValue) { + pTypes.Add (p.DefaultValue); + } else { + AddArguments (paramType, pTypes); + } + + } + + if (type.IsGenericType && !type.IsTypeDefinition) { + viewType = (View)Activator.CreateInstance (type); + Assert.IsType (type, viewType); + } else { + viewType = (View)ctor.Invoke (pTypes.ToArray ()); + Assert.IsType (type, viewType); + } + } + + + return viewType; + } + + private static void AddArguments (Type paramType, List pTypes) + { + if (paramType == typeof (Rect)) { + pTypes.Add (Rect.Empty); + } else if (paramType == typeof (string)) { + pTypes.Add (string.Empty); + } else if (paramType == typeof (int)) { + pTypes.Add (0); + } else if (paramType == typeof (bool)) { + pTypes.Add (true); + } else if (paramType.Name == "IList") { + pTypes.Add (new List ()); + } else if (paramType.Name == "View") { + var top = new Toplevel (); + var view = new View (); + top.Add (view); + pTypes.Add (view); + } else if (paramType.Name == "View[]") { + pTypes.Add (new View [] { }); + } else if (paramType.Name == "Stream") { + pTypes.Add (new MemoryStream ()); + } else if (paramType.Name == "String") { + pTypes.Add (string.Empty); + } else if (paramType.Name == "TreeView`1[T]") { + pTypes.Add (string.Empty); + } else { + pTypes.Add (null); + } + } +} \ No newline at end of file diff --git a/UnitTests/Text/AutocompleteTests.cs b/UnitTests/Text/AutocompleteTests.cs index dcf909fc8f..5f853abbac 100644 --- a/UnitTests/Text/AutocompleteTests.cs +++ b/UnitTests/Text/AutocompleteTests.cs @@ -92,7 +92,7 @@ public void KeyBindings_Command () Assert.Equal ("feature", g.AllSuggestions [^1]); Assert.Equal (0, tv.Autocomplete.SelectedIdx); Assert.Empty (tv.Autocomplete.Suggestions); - Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.F | KeyCode.ShiftMask))); top.Draw (); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); @@ -101,7 +101,7 @@ public void KeyBindings_Command () Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement); Assert.Equal (0, tv.Autocomplete.SelectedIdx); Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorDown))); top.Draw (); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); @@ -110,7 +110,7 @@ public void KeyBindings_Command () Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement); Assert.Equal (1, tv.Autocomplete.SelectedIdx); Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorDown))); top.Draw (); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); @@ -119,7 +119,7 @@ public void KeyBindings_Command () Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement); Assert.Equal (0, tv.Autocomplete.SelectedIdx); Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorUp))); top.Draw (); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); @@ -128,7 +128,7 @@ public void KeyBindings_Command () Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1].Replacement); Assert.Equal (1, tv.Autocomplete.SelectedIdx); Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorUp))); top.Draw (); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); @@ -139,21 +139,21 @@ public void KeyBindings_Command () Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx].Replacement); Assert.True (tv.Autocomplete.Visible); top.Draw (); - Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (tv.Autocomplete.CloseKey))); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); Assert.Empty (tv.Autocomplete.Suggestions); Assert.Equal (3, g.AllSuggestions.Count); Assert.False (tv.Autocomplete.Visible); tv.PositionCursor (); - Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.Reopen, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (tv.Autocomplete.Reopen))); Assert.Equal ($"F Fortunately super feature.", tv.Text); Assert.Equal (new Point (1, 0), tv.CursorPosition); Assert.Equal (2, tv.Autocomplete.Suggestions.Count); Assert.Equal (3, g.AllSuggestions.Count); - Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.SelectionKey, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (tv.Autocomplete.SelectionKey))); tv.PositionCursor (); - Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text); + Assert.Equal ($"fortunately Fortunately super feature.", tv.Text); Assert.Equal (new Point (11, 0), tv.CursorPosition); Assert.Empty (tv.Autocomplete.Suggestions); Assert.Equal (3, g.AllSuggestions.Count); @@ -178,7 +178,7 @@ public void CursorLeft_CursorRight_Mouse_Button_Pressed_Does_Not_Show_Popup () Application.Begin (top); for (int i = 0; i < 7; i++) { - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorRight))); Application.Refresh (); if (i < 4 || i > 5) { TestHelpers.AssertDriverContentsWithFrameAre (@" @@ -202,51 +202,51 @@ This a long line and against TextView. and against ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.g, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.G))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This ag long line and against TextView. against ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorLeft))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This ag long line and against TextView. against ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorLeft))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This ag long line and against TextView. against ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorLeft))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This ag long line and against TextView.", output); for (int i = 0; i < 3; i++) { - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorRight))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This ag long line and against TextView. against ", output); } - Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.Backspace))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This a long line and against TextView. and against ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.n, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.N))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This an long line and against TextView. and ", output); - Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.True (tv.NewKeyDownEvent (new (KeyCode.CursorRight))); Application.Refresh (); TestHelpers.AssertDriverContentsWithFrameAre (@" This an long line and against TextView.", output); diff --git a/UnitTests/Text/CollectionNavigatorTests.cs b/UnitTests/Text/CollectionNavigatorTests.cs index 9031b157a9..f3f2271bb1 100644 --- a/UnitTests/Text/CollectionNavigatorTests.cs +++ b/UnitTests/Text/CollectionNavigatorTests.cs @@ -1,11 +1,18 @@ using System; using System.Threading; -using Terminal.Gui; using Xunit; +using Xunit.Abstractions; -namespace Terminal.Gui.TextTests { - public class CollectionNavigatorTests { - static string [] simpleStrings = new string []{ +namespace Terminal.Gui.TextTests; +public class CollectionNavigatorTests { + readonly ITestOutputHelper _output; + + public CollectionNavigatorTests (ITestOutputHelper output) + { + _output = output; + } + + static string [] simpleStrings = new string []{ "appricot", // 0 "arm", // 1 "bat", // 2 @@ -13,49 +20,49 @@ public class CollectionNavigatorTests { "candle" // 4 }; - [Fact] - public void ShouldAcceptNegativeOne () - { - var n = new CollectionNavigator (simpleStrings); + [Fact] + public void ShouldAcceptNegativeOne () + { + var n = new CollectionNavigator (simpleStrings); - // Expect that index of -1 (i.e. no selection) should work correctly - // and select the first entry of the letter 'b' - Assert.Equal (2, n.GetNextMatchingItem (-1, 'b')); - } - [Fact] - public void OutOfBoundsShouldBeIgnored () - { - var n = new CollectionNavigator (simpleStrings); - - // Expect saying that index 500 is the current selection should not cause - // error and just be ignored (treated as no selection) - Assert.Equal (2, n.GetNextMatchingItem (500, 'b')); - } + // Expect that index of -1 (i.e. no selection) should work correctly + // and select the first entry of the letter 'b' + Assert.Equal (2, n.GetNextMatchingItem (-1, 'b')); + } + [Fact] + public void OutOfBoundsShouldBeIgnored () + { + var n = new CollectionNavigator (simpleStrings); + + // Expect saying that index 500 is the current selection should not cause + // error and just be ignored (treated as no selection) + Assert.Equal (2, n.GetNextMatchingItem (500, 'b')); + } - [Fact] - public void Cycling () - { - // cycling with 'b' - var n = new CollectionNavigator (simpleStrings); - Assert.Equal (2, n.GetNextMatchingItem (0, 'b')); - Assert.Equal (3, n.GetNextMatchingItem (2, 'b')); + [Fact] + public void Cycling () + { + // cycling with 'b' + var n = new CollectionNavigator (simpleStrings); + Assert.Equal (2, n.GetNextMatchingItem (0, 'b')); + Assert.Equal (3, n.GetNextMatchingItem (2, 'b')); - // if 4 (candle) is selected it should loop back to bat - Assert.Equal (2, n.GetNextMatchingItem (4, 'b')); + // if 4 (candle) is selected it should loop back to bat + Assert.Equal (2, n.GetNextMatchingItem (4, 'b')); - // cycling with 'a' - n = new CollectionNavigator (simpleStrings); - Assert.Equal (0, n.GetNextMatchingItem (-1, 'a')); - Assert.Equal (1, n.GetNextMatchingItem (0, 'a')); + // cycling with 'a' + n = new CollectionNavigator (simpleStrings); + Assert.Equal (0, n.GetNextMatchingItem (-1, 'a')); + Assert.Equal (1, n.GetNextMatchingItem (0, 'a')); - // if 4 (candle) is selected it should loop back to appricot - Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); - } + // if 4 (candle) is selected it should loop back to appricot + Assert.Equal (0, n.GetNextMatchingItem (4, 'a')); + } - [Fact] - public void FullText () - { - var strings = new string []{ + [Fact] + public void FullText () + { + var strings = new string []{ "appricot", "arm", "ta", @@ -65,28 +72,28 @@ public void FullText () "candle" }; - var n = new CollectionNavigator (strings); - int current = 0; - Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't')); + var n = new CollectionNavigator (strings); + int current = 0; + Assert.Equal (strings.IndexOf ("ta"), current = n.GetNextMatchingItem (current, 't')); - // should match "te" in "text" - Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'e')); + // should match "te" in "text" + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'e')); - // still matches text - Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x')); + // still matches text + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'x')); - // nothing starts texa so it should NOT jump to appricot - Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a')); + // nothing starts texa so it should NOT jump to appricot + Assert.Equal (strings.IndexOf ("text"), current = n.GetNextMatchingItem (current, 'a')); - Thread.Sleep (n.TypingDelay + 100); - // nothing starts "texa". Since were past timedelay we DO jump to appricot - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - } + Thread.Sleep (n.TypingDelay + 100); + // nothing starts "texa". Since were past timedelay we DO jump to appricot + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + } - [Fact] - public void Unicode () - { - var strings = new string []{ + [Fact] + public void Unicode () + { + var strings = new string []{ "appricot", "arm", "ta", @@ -97,32 +104,32 @@ public void Unicode () "candle" }; - var n = new CollectionNavigator (strings); - int current = 0; - Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗')); + var n = new CollectionNavigator (strings); + int current = 0; + Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丗')); - // 丗丙业丞 is as good a match as 丗丙丛 - // so when doing multi character searches we should - // prefer to stay on the same index unless we invalidate - // our typed text - Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丙')); + // 丗丙业丞 is as good a match as 丗丙丛 + // so when doing multi character searches we should + // prefer to stay on the same index unless we invalidate + // our typed text + Assert.Equal (strings.IndexOf ("丗丙业丞"), current = n.GetNextMatchingItem (current, '丙')); - // No longer matches 丗丙业丞 and now only matches 丗丙丛 - // so we should move to the new match - Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛')); + // No longer matches 丗丙业丞 and now only matches 丗丙丛 + // so we should move to the new match + Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, '丛')); - // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot - Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a')); + // nothing starts "丗丙丛a". Since were still in the timedelay we do not jump to appricot + Assert.Equal (strings.IndexOf ("丗丙丛"), current = n.GetNextMatchingItem (current, 'a')); - Thread.Sleep (n.TypingDelay + 100); - // nothing starts "丗丙丛a". Since were past timedelay we DO jump to appricot - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - } + Thread.Sleep (n.TypingDelay + 100); + // nothing starts "丗丙丛a". Since were past timedelay we DO jump to appricot + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + } - [Fact] - public void AtSymbol () - { - var strings = new string []{ + [Fact] + public void AtSymbol () + { + var strings = new string []{ "appricot", "arm", "ta", @@ -133,16 +140,16 @@ public void AtSymbol () "candle" }; - var n = new CollectionNavigator (strings); - Assert.Equal (3, n.GetNextMatchingItem (0, '@')); - Assert.Equal (3, n.GetNextMatchingItem (3, 'b')); - Assert.Equal (4, n.GetNextMatchingItem (3, 'b')); - } + var n = new CollectionNavigator (strings); + Assert.Equal (3, n.GetNextMatchingItem (0, '@')); + Assert.Equal (3, n.GetNextMatchingItem (3, 'b')); + Assert.Equal (4, n.GetNextMatchingItem (3, 'b')); + } - [Fact] - public void Word () - { - var strings = new string []{ + [Fact] + public void Word () + { + var strings = new string []{ "appricot", "arm", "bat", @@ -150,20 +157,20 @@ public void Word () "bates hotel", "candle" }; - int current = 0; - var n = new CollectionNavigator (strings); - Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat - Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat - Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat - Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel - Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel - Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel - } + int current = 0; + var n = new CollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'b')); // match bat + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 'a')); // match bat + Assert.Equal (strings.IndexOf ("bat"), current = n.GetNextMatchingItem (current, 't')); // match bat + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 'e')); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, 's')); // match bates hotel + Assert.Equal (strings.IndexOf ("bates hotel"), current = n.GetNextMatchingItem (current, ' ')); // match bates hotel + } - [Fact] - public void Symbols () - { - var strings = new string []{ + [Fact] + public void Symbols () + { + var strings = new string []{ "$$", "$100.00", "$101.00", @@ -171,44 +178,44 @@ public void Symbols () "$200.00", "appricot" }; - int current = 0; - var n = new CollectionNavigator (strings); - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); + int current = 0; + var n = new CollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '1')); - Assert.Equal ("$1", n.SearchString); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '1')); + Assert.Equal ("$1", n.SearchString); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '0')); - Assert.Equal ("$10", n.SearchString); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '0')); + Assert.Equal ("$10", n.SearchString); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '1')); - Assert.Equal ("$101", n.SearchString); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '1')); + Assert.Equal ("$101", n.SearchString); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.')); - Assert.Equal ("$101.", n.SearchString); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '.')); + Assert.Equal ("$101.", n.SearchString); - // stay on the same item becuase still in timedelay - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("$101.", n.SearchString); + // stay on the same item becuase still in timedelay + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("$101.", n.SearchString); - Thread.Sleep (n.TypingDelay + 100); - // another '$' means searching for "$" again - Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); + Thread.Sleep (n.TypingDelay + 100); + // another '$' means searching for "$" again + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$$", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$$", n.SearchString); - } + } - [Fact] - public void Delay () - { - var strings = new string []{ + [Fact] + public void Delay () + { + var strings = new string []{ "$$", "$100.00", "$101.00", @@ -216,47 +223,47 @@ public void Delay () "$200.00", "appricot" }; - int current = 0; - var n = new CollectionNavigator (strings); - - // No delay - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$$", n.SearchString); - - // Delay - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); - - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); - - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); - - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); - - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); - Assert.Equal ("$", n.SearchString); - - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '2')); // Shouldn't move - Assert.Equal ("2", n.SearchString); - } + int current = 0; + var n = new CollectionNavigator (strings); + + // No delay + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$$", n.SearchString); + + // Delay + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '$')); + Assert.Equal ("$", n.SearchString); + + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, '2')); // Shouldn't move + Assert.Equal ("2", n.SearchString); + } - [Fact] - public void MutliKeySearchPlusWrongKeyStays () - { - var strings = new string []{ + [Fact] + public void MutliKeySearchPlusWrongKeyStays () + { + var strings = new string []{ "a", "c", "can", @@ -265,47 +272,47 @@ public void MutliKeySearchPlusWrongKeyStays () "yellow", "zebra" }; - int current = 0; - var n = new CollectionNavigator (strings); - - // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573 - // One thing that it currently does that is different from Explorer is that as soon as you hit a wrong key then it jumps to that index. - // So if you type cand then z it jumps you to something beginning with z. In the same situation Windows Explorer beeps (not the best!) - // but remains on candle. - // We might be able to update the behaviour so that a 'wrong' keypress (z) within 500ms of a 'right' keypress ("can" + 'd') is - // simply ignored (possibly ending the search process though). That would give a short delay for user to realise the thing - // they typed doesn't exist and then start a new search (which would be possible 500ms after the last 'good' keypress). - // This would only apply for 2+ character searches where theres been a successful 2+ character match right before. - - Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); - Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); - Assert.Equal ("c", n.SearchString); - Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("ca", n.SearchString); - Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); - Assert.Equal ("can", n.SearchString); - Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'd')); - Assert.Equal ("cand", n.SearchString); - - // Same as above, but with a 'wrong' key (z) - Thread.Sleep (n.TypingDelay + 10); - Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("a", n.SearchString); - Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); - Assert.Equal ("c", n.SearchString); - Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); - Assert.Equal ("ca", n.SearchString); - Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); - Assert.Equal ("can", n.SearchString); - Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'z')); // Shouldn't move - Assert.Equal ("can", n.SearchString); // Shouldn't change - } + int current = 0; + var n = new CollectionNavigator (strings); + + // https://github.com/gui-cs/Terminal.Gui/pull/2132#issuecomment-1298425573 + // One thing that it currently does that is different from Explorer is that as soon as you hit a wrong key then it jumps to that index. + // So if you type cand then z it jumps you to something beginning with z. In the same situation Windows Explorer beeps (not the best!) + // but remains on candle. + // We might be able to update the behaviour so that a 'wrong' keypress (z) within 500ms of a 'right' keypress ("can" + 'd') is + // simply ignored (possibly ending the search process though). That would give a short delay for user to realise the thing + // they typed doesn't exist and then start a new search (which would be possible 500ms after the last 'good' keypress). + // This would only apply for 2+ character searches where theres been a successful 2+ character match right before. + + Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); + Assert.Equal ("c", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("ca", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); + Assert.Equal ("can", n.SearchString); + Assert.Equal (strings.IndexOf ("candle"), current = n.GetNextMatchingItem (current, 'd')); + Assert.Equal ("cand", n.SearchString); + + // Same as above, but with a 'wrong' key (z) + Thread.Sleep (n.TypingDelay + 10); + Assert.Equal (strings.IndexOf ("a"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("a", n.SearchString); + Assert.Equal (strings.IndexOf ("c"), current = n.GetNextMatchingItem (current, 'c')); + Assert.Equal ("c", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'a')); + Assert.Equal ("ca", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'n')); + Assert.Equal ("can", n.SearchString); + Assert.Equal (strings.IndexOf ("can"), current = n.GetNextMatchingItem (current, 'z')); // Shouldn't move + Assert.Equal ("can", n.SearchString); // Shouldn't change + } - [Fact] - public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () - { - var strings = new string [] { + [Fact] + public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () + { + var strings = new string [] { "$$", "$100.00", "$101.00", @@ -316,42 +323,42 @@ public void MinimizeMovement_False_ShouldMoveIfMultipleMatches () "car", "cart", }; - int current = 0; - var n = new CollectionNavigator (strings); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); // back to top - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); - Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, "$", false)); - Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$", false)); - - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top - Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, "a", false)); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top - - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00", false)); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); - Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); - - Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$200.00", false)); - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); - Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); - - Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); - Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); - - Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", false)); - Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car", false)); - - Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", false)); - } + int current = 0; + var n = new CollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", false)); // back to top + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.10"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$", false)); + + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top + Assert.Equal (strings.IndexOf ("appricot"), current = n.GetNextMatchingItem (current, "a", false)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", false)); // back to top + + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$100.00", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$200.00", false)); + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + + Assert.Equal (strings.IndexOf ("$101.00"), current = n.GetNextMatchingItem (current, "$101.00", false)); + Assert.Equal (strings.IndexOf ("$200.00"), current = n.GetNextMatchingItem (current, "$2", false)); + + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", false)); + Assert.Equal (strings.IndexOf ("cart"), current = n.GetNextMatchingItem (current, "car", false)); + + Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", false)); + } - [Fact] - public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () - { - var strings = new string [] { + [Fact] + public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () + { + var strings = new string [] { "$$", "$100.00", "$101.00", @@ -362,47 +369,36 @@ public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () "car", "cart", }; - int current = 0; - var n = new CollectionNavigator (strings); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true)); - Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$1", true)); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); - Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); - - Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); - Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); - - Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true)); - } + int current = 0; + var n = new CollectionNavigator (strings); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$", true)); + Assert.Equal (strings.IndexOf ("$$"), current = n.GetNextMatchingItem (current, "$$", true)); // back to top + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$1", true)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); + Assert.Equal (strings.IndexOf ("$100.00"), current = n.GetNextMatchingItem (current, "$", true)); + + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); + Assert.Equal (strings.IndexOf ("car"), current = n.GetNextMatchingItem (current, "car", true)); + + Assert.Equal (-1, current = n.GetNextMatchingItem (current, "x", true)); + } - [Fact] - public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys () - { - // test all Keys - foreach (Key key in Enum.GetValues (typeof (Key))) { - var ke = new KeyEvent (key, new KeyModifiers () { - Alt = key == Key.AltMask, - Ctrl = key == Key.CtrlMask, - Shift = key == Key.ShiftMask - }); - if (key == Key.AltMask || key == Key.CtrlMask) { - Assert.False (CollectionNavigator.IsCompatibleKey (ke)); - } else { - Assert.True (CollectionNavigator.IsCompatibleKey (ke)); - } + [Fact] + public void IsCompatibleKey_Does_Not_Allow_Alt_And_Ctrl_Keys () + { + // test all Keys + foreach (KeyCode key in Enum.GetValues (typeof (KeyCode))) { + var ke = new Key (key); + _output.WriteLine ($"Testing {key}"); + if (key == KeyCode.AltMask || key == KeyCode.CtrlMask || key == KeyCode.SpecialMask) { + Assert.False (CollectionNavigator.IsCompatibleKey (ke)); + } else { + Assert.True (CollectionNavigator.IsCompatibleKey (ke)); } - - // test Capslock, Numlock and Scrolllock - Assert.True (CollectionNavigator.IsCompatibleKey (new KeyEvent (Key.Null, new KeyModifiers () { - Alt = false, - Ctrl = false, - Shift = false, - Capslock = true, - Numlock = true, - Scrolllock = true, - }))); } + + // test Capslock, Numlock and Scrolllock + Assert.True (CollectionNavigator.IsCompatibleKey (new (KeyCode.Null))); } -} +} \ No newline at end of file diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 99756f0f1f..b6b7969bbd 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -227,33 +227,33 @@ public void FindHotKey_Invalid_ReturnsFalse (string text) Rune hotKeySpecifier = (Rune)'_'; bool supportFirstUpperCase = false; int hotPos = 0; - Key hotKey = Key.Unknown; + Key hotKey = KeyCode.Null; bool result = false; result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out hotPos, out hotKey); Assert.False (result); Assert.Equal (-1, hotPos); - Assert.Equal (Key.Unknown, hotKey); - } - - [Theory] - [InlineData ("_K Before", true, 0, (Key)'K')] - [InlineData ("a_K Second", true, 1, (Key)'K')] - [InlineData ("Last _K", true, 5, (Key)'K')] - [InlineData ("After K_", false, -1, Key.Unknown)] - [InlineData ("Multiple _K and _R", true, 9, (Key)'K')] - [InlineData ("Non-english: _Кдать", true, 13, (Key)'К')] // Cryllic K (К) - [InlineData ("_K Before", true, 0, (Key)'K', true)] // Turn on FirstUpperCase and verify same results - [InlineData ("a_K Second", true, 1, (Key)'K', true)] - [InlineData ("Last _K", true, 5, (Key)'K', true)] - [InlineData ("After K_", false, -1, Key.Unknown, true)] - [InlineData ("Multiple _K and _R", true, 9, (Key)'K', true)] - [InlineData ("Non-english: _Кдать", true, 13, (Key)'К', true)] // Cryllic K (К) + Assert.Equal (KeyCode.Null, hotKey); + } + + [Theory] + [InlineData ("_K Before", true, 0, (KeyCode)'K')] + [InlineData ("a_K Second", true, 1, (KeyCode)'K')] + [InlineData ("Last _K", true, 5, (KeyCode)'K')] + [InlineData ("After K_", false, -1, KeyCode.Null)] + [InlineData ("Multiple _K and _R", true, 9, (KeyCode)'K')] + [InlineData ("Non-english: _Кдать", true, 13, (KeyCode)'К')] // Cryllic K (К) + [InlineData ("_K Before", true, 0, (KeyCode)'K', true)] // Turn on FirstUpperCase and verify same results + [InlineData ("a_K Second", true, 1, (KeyCode)'K', true)] + [InlineData ("Last _K", true, 5, (KeyCode)'K', true)] + [InlineData ("After K_", false, -1, KeyCode.Null, true)] + [InlineData ("Multiple _K and _R", true, 9, (KeyCode)'K', true)] + [InlineData ("Non-english: _Кдать", true, 13, (KeyCode)'К', true)] // Cryllic K (К) public void FindHotKey_AlphaUpperCase_Succeeds (string text, bool expectedResult, int expectedHotPos, Key expectedKey, bool supportFirstUpperCase = false) { Rune hotKeySpecifier = (Rune)'_'; - var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out Key hotKey); + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out var hotKey); if (expectedResult) { Assert.True (result); } else { @@ -265,23 +265,23 @@ public void FindHotKey_AlphaUpperCase_Succeeds (string text, bool expectedResult } [Theory] - [InlineData ("_k Before", true, 0, (Key)'K')] // lower case should return uppercase Hotkey - [InlineData ("a_k Second", true, 1, (Key)'K')] - [InlineData ("Last _k", true, 5, (Key)'K')] - [InlineData ("After k_", false, -1, Key.Unknown)] - [InlineData ("Multiple _k and _R", true, 9, (Key)'K')] - [InlineData ("Non-english: _кдать", true, 13, (Key)'К')] // Lower case Cryllic K (к) - [InlineData ("_k Before", true, 0, (Key)'K', true)] // Turn on FirstUpperCase and verify same results - [InlineData ("a_k Second", true, 1, (Key)'K', true)] - [InlineData ("Last _k", true, 5, (Key)'K', true)] - [InlineData ("After k_", false, -1, Key.Unknown, true)] - [InlineData ("Multiple _k and _r", true, 9, (Key)'K', true)] - [InlineData ("Non-english: _кдать", true, 13, (Key)'К', true)] // Cryllic K (К) + [InlineData ("_k Before", true, 0, (KeyCode)'K')] // lower case should return uppercase Hotkey + [InlineData ("a_k Second", true, 1, (KeyCode)'K')] + [InlineData ("Last _k", true, 5, (KeyCode)'K')] + [InlineData ("After k_", false, -1, KeyCode.Null)] + [InlineData ("Multiple _k and _R", true, 9, (KeyCode)'K')] + [InlineData ("Non-english: _кдать", true, 13, (KeyCode)'к')] // Lower case Cryllic K (к) + [InlineData ("_k Before", true, 0, (KeyCode)'K', true)] // Turn on FirstUpperCase and verify same results + [InlineData ("a_k Second", true, 1, (KeyCode)'K', true)] + [InlineData ("Last _k", true, 5, (KeyCode)'K', true)] + [InlineData ("After k_", false, -1, KeyCode.Null, true)] + [InlineData ("Multiple _k and _r", true, 9, (KeyCode)'K', true)] + [InlineData ("Non-english: _кдать", true, 13, (KeyCode)'к', true)] // Cryllic K (К) public void FindHotKey_AlphaLowerCase_Succeeds (string text, bool expectedResult, int expectedHotPos, Key expectedKey, bool supportFirstUpperCase = false) { Rune hotKeySpecifier = (Rune)'_'; - var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out Key hotKey); + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out var hotKey); if (expectedResult) { Assert.True (result); } else { @@ -293,21 +293,21 @@ public void FindHotKey_AlphaLowerCase_Succeeds (string text, bool expectedResult } [Theory] - [InlineData ("_1 Before", true, 0, (Key)'1')] // Digits - [InlineData ("a_1 Second", true, 1, (Key)'1')] - [InlineData ("Last _1", true, 5, (Key)'1')] - [InlineData ("After 1_", false, -1, Key.Unknown)] - [InlineData ("Multiple _1 and _2", true, 9, (Key)'1')] - [InlineData ("_1 Before", true, 0, (Key)'1', true)] // Turn on FirstUpperCase and verify same results - [InlineData ("a_1 Second", true, 1, (Key)'1', true)] - [InlineData ("Last _1", true, 5, (Key)'1', true)] - [InlineData ("After 1_", false, -1, Key.Unknown, true)] - [InlineData ("Multiple _1 and _2", true, 9, (Key)'1', true)] + [InlineData ("_1 Before", true, 0, (KeyCode)'1')] // Digits + [InlineData ("a_1 Second", true, 1, (KeyCode)'1')] + [InlineData ("Last _1", true, 5, (KeyCode)'1')] + [InlineData ("After 1_", false, -1, KeyCode.Null)] + [InlineData ("Multiple _1 and _2", true, 9, (KeyCode)'1')] + [InlineData ("_1 Before", true, 0, (KeyCode)'1', true)] // Turn on FirstUpperCase and verify same results + [InlineData ("a_1 Second", true, 1, (KeyCode)'1', true)] + [InlineData ("Last _1", true, 5, (KeyCode)'1', true)] + [InlineData ("After 1_", false, -1, KeyCode.Null, true)] + [InlineData ("Multiple _1 and _2", true, 9, (KeyCode)'1', true)] public void FindHotKey_Numeric_Succeeds (string text, bool expectedResult, int expectedHotPos, Key expectedKey, bool supportFirstUpperCase = false) { Rune hotKeySpecifier = (Rune)'_'; - var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out Key hotKey); + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out var hotKey); if (expectedResult) { Assert.True (result); } else { @@ -319,18 +319,18 @@ public void FindHotKey_Numeric_Succeeds (string text, bool expectedResult, int e } [Theory] - [InlineData ("K Before", true, 0, (Key)'K')] - [InlineData ("aK Second", true, 1, (Key)'K')] - [InlineData ("last K", true, 5, (Key)'K')] - [InlineData ("multiple K and R", true, 9, (Key)'K')] - [InlineData ("non-english: Кдать", true, 13, (Key)'К')] // Cryllic K (К) + [InlineData ("K Before", true, 0, (KeyCode)'K')] + [InlineData ("aK Second", true, 1, (KeyCode)'K')] + [InlineData ("last K", true, 5, (KeyCode)'K')] + [InlineData ("multiple K and R", true, 9, (KeyCode)'K')] + [InlineData ("non-english: Кдать", true, 13, (KeyCode)'К')] // Cryllic K (К) public void FindHotKey_Legacy_FirstUpperCase_Succeeds (string text, bool expectedResult, int expectedHotPos, Key expectedKey) { var supportFirstUpperCase = true; Rune hotKeySpecifier = (Rune)0; - var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out Key hotKey); + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out var hotKey); if (expectedResult) { Assert.True (result); } else { @@ -340,6 +340,24 @@ public void FindHotKey_Legacy_FirstUpperCase_Succeeds (string text, bool expecte Assert.Equal (expectedHotPos, hotPos); Assert.Equal (expectedKey, hotKey); } + + [Theory] + //[InlineData ("_\"k before", false, Key.Null)] // BUGBUG: Not sure why this fails + [InlineData ("\"_k before", true, KeyCode.K)] + [InlineData ("_`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?", true, (KeyCode)'`')] + [InlineData ("`_~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?", true, (KeyCode)'~')] + //[InlineData ("`~!@#$%^&*()-__=+[{]}\\|;:'\",<.>/?", true, (Key)'_')] // BUGBUG: Not sure why this fails + [InlineData ("_ ~  s  gui.cs   master ↑10", true, (KeyCode)'')] // ~IsLetterOrDigit + Unicode + [InlineData (" ~  s  gui.cs  _ master ↑10", true, (KeyCode)'')] // ~IsLetterOrDigit + Unicode + [InlineData ("non-english: _кдать", true, (KeyCode)'к')] // Lower case Cryllic K (к) + public void FindHotKey_Symbols_Returns_Symbol (string text, bool found, Key expected) + { + var hotKeySpecifier = (Rune)'_'; + + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, false, out int _, out var hotKey); + Assert.Equal (found, result); + Assert.Equal (expected, hotKey); + } [Theory] [InlineData ("\"k before")] @@ -356,10 +374,10 @@ public void FindHotKey_Legacy_FirstUpperCase_NotFound_Returns_False (string text var hotKeySpecifier = (Rune)0; - var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out Key hotKey); + var result = TextFormatter.FindHotKey (text, hotKeySpecifier, supportFirstUpperCase, out int hotPos, out var hotKey); Assert.False (result); Assert.Equal (-1, hotPos); - Assert.Equal (Key.Unknown, hotKey); + Assert.Equal (KeyCode.Null, hotKey); } [Theory] @@ -1438,9 +1456,9 @@ public void TestClipOrPad_LongWord (string text, int fillPad, string expectedTex public void Internal_Tests () { var tf = new TextFormatter (); - Assert.Equal (Key.Null, tf.HotKey); - tf.HotKey = Key.CtrlMask | Key.Q; - Assert.Equal (Key.CtrlMask | Key.Q, tf.HotKey); + Assert.Equal (KeyCode.Null, tf.HotKey); + tf.HotKey = KeyCode.CtrlMask | KeyCode.Q; + Assert.Equal (KeyCode.CtrlMask | KeyCode.Q, tf.HotKey); } [Theory] diff --git a/UnitTests/UICatalog/ScenarioTests.cs b/UnitTests/UICatalog/ScenarioTests.cs index f951073537..6a971ab3c6 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -29,13 +29,13 @@ int CreateInput (string input) { FakeConsole.MockKeyPresses.Clear (); // Put a QuitKey in at the end - FakeConsole.PushMockKeyPress (Application.QuitKey); + FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey); foreach (var c in input.Reverse ()) { - Key key = Key.Unknown; + KeyCode key = KeyCode.Unknown; if (char.IsLetter (c)) { - key = (Key)char.ToUpper (c) | (char.IsUpper (c) ? Key.ShiftMask : (Key)0); + key = (KeyCode)char.ToUpper (c) | (char.IsUpper (c) ? KeyCode.ShiftMask : (KeyCode)0); } else { - key = (Key)c; + key = (KeyCode)c; } FakeConsole.PushMockKeyPress (key); } @@ -66,15 +66,15 @@ public void Run_All_Scenarios () // BUGBUG: (#2474) For some reason ReadKey is not returning the QuitKey for some Scenarios // by adding this Space it seems to work. //FakeConsole.PushMockKeyPress (Key.Space); - FakeConsole.PushMockKeyPress (Application.QuitKey); + FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey); // The only key we care about is the QuitKey - Application.Top.KeyPressed += (object sender, KeyEventEventArgs args) => { - output.WriteLine ($" Keypress: {args.KeyEvent.Key}"); + Application.Top.KeyDown += (object sender, Key args) => { + output.WriteLine ($" Keypress: {args.KeyCode}"); // BUGBUG: (#2474) For some reason ReadKey is not returning the QuitKey for some Scenarios // by adding this Space it seems to work. // See #2474 for why this is commented out - Assert.Equal (Application.QuitKey, args.KeyEvent.Key); + Assert.Equal (Application.QuitKey.KeyCode, args.KeyCode); }; uint abortTime = 500; @@ -126,7 +126,7 @@ public void Run_Generic () // BUGBUG: (#2474) For some reason ReadKey is not returning the QuitKey for some Scenarios // by adding this Space it seems to work. - FakeConsole.PushMockKeyPress (Application.QuitKey); + FakeConsole.PushMockKeyPress ((KeyCode)Application.QuitKey); var ms = 100; var abortCount = 0; @@ -153,9 +153,9 @@ public void Run_Generic () } }; - Application.Top.KeyPressed += (object sender, KeyEventEventArgs args) => { + Application.Top.KeyDown += (object sender, Key args) => { // See #2474 for why this is commented out - Assert.Equal (Key.CtrlMask | Key.Q, args.KeyEvent.Key); + Assert.Equal (KeyCode.CtrlMask | KeyCode.Q, args.KeyCode); }; generic.Init (); diff --git a/UnitTests/UnitTests.csproj b/UnitTests/UnitTests.csproj index 06c25a8810..0da497ab80 100644 --- a/UnitTests/UnitTests.csproj +++ b/UnitTests/UnitTests.csproj @@ -1,6 +1,6 @@  - net7.0 + net8.0 Preview @@ -24,9 +24,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/UnitTests/View/HotKeyTests.cs b/UnitTests/View/HotKeyTests.cs new file mode 100644 index 0000000000..21e8775f7e --- /dev/null +++ b/UnitTests/View/HotKeyTests.cs @@ -0,0 +1,307 @@ +using System; +using Xunit; +using Xunit.Abstractions; +using System.Text; + +namespace Terminal.Gui.ViewTests; + +public class HotKeyTests { + readonly ITestOutputHelper _output; + + public HotKeyTests (ITestOutputHelper output) + { + this._output = output; + } + + [Fact] + public void Defaults () + { + var view = new View (); + Assert.Equal (string.Empty, view.Title); + Assert.Equal (KeyCode.Null, view.HotKey); + + // Verify key bindings were set + var commands = view.KeyBindings.GetCommands (KeyCode.Null); + Assert.Empty (commands); + } + + [Theory] + [InlineData (KeyCode.A)] + [InlineData ((KeyCode)'a')] + [InlineData (KeyCode.A | KeyCode.ShiftMask)] + [InlineData (KeyCode.D1)] + [InlineData (KeyCode.D1 | KeyCode.ShiftMask)] + [InlineData ((KeyCode)'!')] + [InlineData ((KeyCode)'х')] // Cyrillic x + [InlineData ((KeyCode)'你')] // Chinese ni + [InlineData ((KeyCode)'ö')] // German o umlaut + [InlineData (KeyCode.Null)] + public void Set_Sets_WithValidKey (KeyCode key) + { + var view = new View (); + view.HotKey = key; + Assert.Equal (key, view.HotKey); + } + + [Theory] + [InlineData (KeyCode.A)] + [InlineData (KeyCode.A | KeyCode.ShiftMask)] + [InlineData (KeyCode.D1)] + [InlineData (KeyCode.D1 | KeyCode.ShiftMask)] // '!' + [InlineData ((KeyCode)'х')] // Cyrillic x + [InlineData ((KeyCode)'你')] // Chinese ni + [InlineData ((KeyCode)'ö')] // German o umlaut + public void Set_SetsKeyBindings (Key key) + { + var view = new View (); + view.HotKey = key; + Assert.Equal (string.Empty, view.Title); + Assert.Equal (key, view.HotKey); + + // Verify key bindings were set + + // As passed + var commands = view.KeyBindings.GetCommands (key); + Assert.Contains (Command.Accept, commands); + + var baseKey = key.NoShift; + // If A...Z, with and without shift + if (baseKey.IsKeyCodeAtoZ) { + commands = view.KeyBindings.GetCommands (key.WithShift); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key.NoShift); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key.WithAlt); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key.NoShift.WithAlt); + Assert.Contains (Command.Accept, commands); + } else { + // Non A..Z keys should not have shift bindings + if (key.IsShift) { + commands = view.KeyBindings.GetCommands (key.NoShift); + Assert.Empty (commands); + } else { + commands = view.KeyBindings.GetCommands (key.WithShift); + Assert.Empty (commands); + } + } + } + + [Fact] + public void Set_RemovesOldKeyBindings () + { + var view = new View (); + view.HotKey = KeyCode.A; + Assert.Equal (string.Empty, view.Title); + Assert.Equal (KeyCode.A, view.HotKey); + + // Verify key bindings were set + var commands = view.KeyBindings.GetCommands (KeyCode.A); + Assert.Contains (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); + Assert.Contains (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); + Assert.Contains (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); + Assert.Contains (Command.Accept, commands); + + // Now set again + view.HotKey = KeyCode.B; + Assert.Equal (string.Empty, view.Title); + Assert.Equal (KeyCode.B, view.HotKey); + + commands = view.KeyBindings.GetCommands (KeyCode.A); + Assert.DoesNotContain (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask); + Assert.DoesNotContain (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.AltMask); + Assert.DoesNotContain (Command.Accept, commands); + + commands = view.KeyBindings.GetCommands (KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask); + Assert.DoesNotContain (Command.Accept, commands); + } + + [Fact] + public void Set_Throws_If_Modifiers_Are_Included () + { + var view = new View (); + // A..Z must be naked (Alt is assumed) + view.HotKey = KeyCode.A | KeyCode.AltMask; + Assert.Throws (() => view.HotKey = KeyCode.A | KeyCode.CtrlMask); + Assert.Throws (() => view.HotKey = KeyCode.A | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask); + + // All others must not have Ctrl (Alt is assumed) + view.HotKey = KeyCode.D1 | KeyCode.AltMask; + Assert.Throws (() => view.HotKey = KeyCode.D1 | KeyCode.CtrlMask); + Assert.Throws (() => view.HotKey = KeyCode.D1 | KeyCode.ShiftMask | KeyCode.AltMask | KeyCode.CtrlMask); + + // Shift is ok (e.g. this is '!') + view.HotKey = KeyCode.D1 | KeyCode.ShiftMask; + } + + [Theory] + [InlineData (KeyCode.A)] + [InlineData (KeyCode.A | KeyCode.ShiftMask)] + [InlineData (KeyCode.D1)] + [InlineData (KeyCode.D1 | KeyCode.ShiftMask)] // '!' + [InlineData ((KeyCode)'х')] // Cyrillic x + [InlineData ((KeyCode)'你')] // Chinese ni + public void AddKeyBindingsForHotKey_Sets (KeyCode key) + { + var view = new View (); + view.HotKey = KeyCode.Z; + Assert.Equal (string.Empty, view.Title); + Assert.Equal (KeyCode.Z, view.HotKey); + + view.AddKeyBindingsForHotKey (KeyCode.Null, key); + + // Verify key bindings were set + + // As passed + var commands = view.KeyBindings.GetCommands (key); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask); + Assert.Contains (Command.Accept, commands); + + var baseKey = key & ~KeyCode.ShiftMask; + // If A...Z, with and without shift + if (baseKey is >= KeyCode.A and <= KeyCode.Z) { + commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key | KeyCode.AltMask); + Assert.Contains (Command.Accept, commands); + commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask | KeyCode.AltMask); + Assert.Contains (Command.Accept, commands); + } else { + // Non A..Z keys should not have shift bindings + if (key.HasFlag (KeyCode.ShiftMask)) { + commands = view.KeyBindings.GetCommands (key & ~KeyCode.ShiftMask); + Assert.Empty (commands); + } else { + commands = view.KeyBindings.GetCommands (key | KeyCode.ShiftMask); + Assert.Empty (commands); + } + } + } + + [Theory] + [InlineData (KeyCode.Delete)] + [InlineData (KeyCode.Backspace)] + [InlineData (KeyCode.Tab)] + [InlineData (KeyCode.Enter)] + [InlineData (KeyCode.Esc)] + [InlineData (KeyCode.Space)] + [InlineData (KeyCode.CursorLeft)] + [InlineData (KeyCode.F1)] + [InlineData (KeyCode.Unknown)] + public void Set_Throws_With_Invalid_Key (KeyCode key) + { + var view = new View (); + Assert.Throws (() => view.HotKey = key); + } + + [Theory] + [InlineData ("Test", KeyCode.T)] + [InlineData ("^Test", KeyCode.T)] + [InlineData ("T^est", KeyCode.E)] + [InlineData ("Te^st", KeyCode.S)] + [InlineData ("Tes^t", KeyCode.T)] + [InlineData ("other", KeyCode.Null)] + [InlineData ("oTher", KeyCode.T)] + [InlineData ("^Öther", (KeyCode)'Ö')] + [InlineData ("^öther", (KeyCode)'ö')] + // BUGBUG: '!' should be supported. Line 968 of TextFormatter filters on char.IsLetterOrDigit + //[InlineData ("Test^!", (Key)'!')] + public void Text_Change_Sets_HotKey (string text, KeyCode expectedHotKey) + { + var view = new View () { + HotKeySpecifier = new Rune ('^'), + Text = "^Hello" + }; + Assert.Equal (KeyCode.H, view.HotKey); + + view.Text = text; + Assert.Equal (expectedHotKey, view.HotKey); + + } + + [Theory] + [InlineData("^Test")] + public void Text_Empty_Sets_HotKey_To_Null (string text) + { + var view = new View () { + HotKeySpecifier = (Rune)'^', + Text = text + }; + + Assert.Equal (text, view.Text); + Assert.Equal (KeyCode.T, view.HotKey); + + view.Text = string.Empty; + Assert.Equal ("", view.Text); + Assert.Equal (KeyCode.Null, view.HotKey); + } + + [Theory] + [InlineData (KeyCode.Null, true)] // non-shift + [InlineData (KeyCode.ShiftMask, true)] + [InlineData (KeyCode.AltMask, true)] + [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) + { + var view = new View () { + HotKeySpecifier = (Rune)'^', + Text = "^Test" + }; + view.CanFocus = true; + Assert.False (view.HasFocus); + view.NewKeyDownEvent (new (KeyCode.T | mask)); + Assert.Equal (expected, view.HasFocus); + } + + [Fact] + public void ProcessKeyDown_Invokes_HotKey_Command_With_SuperView () + { + var view = new View () { + HotKeySpecifier = (Rune)'^', + Text = "^Test" + }; + + var superView = new View (); + superView.Add (view); + + view.CanFocus = true; + Assert.False (view.HasFocus); + + var ke = new Key (KeyCode.T); + superView.NewKeyDownEvent (ke); + Assert.True (view.HasFocus); + + } + + + [Fact] + public void ProcessKeyDown_Ignores_KeyBindings_Out_Of_Scope_SuperView () + { + var view = new View (); + view.KeyBindings.Add (KeyCode.A, Command.Default); + view.InvokingKeyBindings += (s, e) => { + Assert.Fail (); + }; + + var superView = new View (); + superView.Add (view); + + var ke = new Key (KeyCode.A); + superView.NewKeyDownEvent (ke); + } +} \ No newline at end of file diff --git a/UnitTests/View/KeyboardEventTests.cs b/UnitTests/View/KeyboardEventTests.cs new file mode 100644 index 0000000000..fd8edf0353 --- /dev/null +++ b/UnitTests/View/KeyboardEventTests.cs @@ -0,0 +1,458 @@ +using System; +using Xunit; +using Xunit.Abstractions; + +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; + +namespace Terminal.Gui.ViewTests; + +public class KeyboardEventTests { + readonly ITestOutputHelper _output; + + public KeyboardEventTests (ITestOutputHelper output) => _output = output; + + [Fact] + public void KeyPress_Handled_Cancels () + { + var view = new View (); + bool invokingKeyBindingsInvoked = false; + bool processKeyPressInvoked = false; + bool setHandledTo = false; + + view.KeyDown += (s, e) => { + e.Handled = setHandledTo; + Assert.Equal (setHandledTo, e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.InvokingKeyBindings += (s, e) => { + invokingKeyBindingsInvoked = true; + Assert.False (e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.ProcessKeyDown += (s, e) => { + processKeyPressInvoked = true; + Assert.False (e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.NewKeyDownEvent (new Key (KeyCode.N)); + Assert.True (invokingKeyBindingsInvoked); + Assert.True (processKeyPressInvoked); + + invokingKeyBindingsInvoked = false; + processKeyPressInvoked = false; + setHandledTo = true; + view.NewKeyDownEvent (new Key (KeyCode.N)); + Assert.False (invokingKeyBindingsInvoked); + Assert.False (processKeyPressInvoked); + } + + [Fact] + public void InvokingKeyBindings_Handled_Cancels () + { + var view = new View (); + bool keyPressInvoked = false; + bool invokingKeyBindingsInvoked = false; + bool processKeyPressInvoked = false; + bool setHandledTo = false; + + view.KeyDown += (s, e) => { + keyPressInvoked = true; + Assert.False (e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.InvokingKeyBindings += (s, e) => { + invokingKeyBindingsInvoked = true; + e.Handled = setHandledTo; + Assert.Equal (setHandledTo, e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.ProcessKeyDown += (s, e) => { + processKeyPressInvoked = true; + processKeyPressInvoked = true; + Assert.False (e.Handled); + Assert.Equal (KeyCode.N, e.KeyCode); + }; + + view.NewKeyDownEvent (new Key (KeyCode.N)); + Assert.True (keyPressInvoked); + Assert.True (invokingKeyBindingsInvoked); + Assert.True (processKeyPressInvoked); + + keyPressInvoked = false; + invokingKeyBindingsInvoked = false; + processKeyPressInvoked = false; + setHandledTo = true; + view.NewKeyDownEvent (new Key (KeyCode.N)); + Assert.True (keyPressInvoked); + Assert.True (invokingKeyBindingsInvoked); + Assert.False (processKeyPressInvoked); + } + + [Theory] + [InlineData (null, null)] + [InlineData (true, true)] + [InlineData (false, false)] + public void OnInvokingKeyBindings_Returns_Nullable_Properly (bool? toReturn, bool? expected) + { + var view = new KeyBindingsTestView (); + view.CommandReturns = toReturn; + + bool? result = view.OnInvokingKeyBindings (new Key (KeyCode.A)); + Assert.Equal (expected, result); + } + + /// + /// A view that overrides the OnKey* methods so we can test that they are called. + /// + public class KeyBindingsTestView : View { + public bool? CommandReturns { get; set; } + + public KeyBindingsTestView () + { + CanFocus = true; + AddCommand (Command.Default, () => CommandReturns); + KeyBindings.Add (KeyCode.A, Command.Default); + } + } + + [Fact] + public void KeyDown_Handled_True_Stops_Processing () + { + bool keyDown = false; + bool invokingKeyBindings = false; + bool keyPressed = false; + + var view = new OnKeyTestView (); + Assert.True (view.CanFocus); + view.CancelVirtualMethods = false; + + view.KeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyDown); + Assert.False (view.OnKeyDownContinued); + e.Handled = true; + keyDown = true; + }; + view.InvokingKeyBindings += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnInvokingKeyBindingsContinued); + e.Handled = true; + invokingKeyBindings = true; + }; + view.ProcessKeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnKeyPressedContinued); + e.Handled = true; + keyPressed = true; + }; + + view.NewKeyDownEvent (new Key (KeyCode.A)); + Assert.True (keyDown); + Assert.False (invokingKeyBindings); + Assert.False (keyPressed); + + Assert.False (view.OnKeyDownContinued); + Assert.False (view.OnInvokingKeyBindingsContinued); + Assert.False (view.OnKeyPressedContinued); + } + + [Fact] + public void InvokingKeyBindings_Handled_True_Stops_Processing () + { + bool keyDown = false; + bool invokingKeyBindings = false; + bool keyPressed = false; + + var view = new OnKeyTestView (); + Assert.True (view.CanFocus); + view.CancelVirtualMethods = false; + + view.KeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyDown); + Assert.False (view.OnKeyDownContinued); + e.Handled = false; + keyDown = true; + }; + view.InvokingKeyBindings += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnInvokingKeyBindingsContinued); + e.Handled = true; + invokingKeyBindings = true; + }; + view.ProcessKeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnKeyPressedContinued); + e.Handled = true; + keyPressed = true; + }; + + view.NewKeyDownEvent (new Key (KeyCode.A)); + Assert.True (keyDown); + Assert.True (invokingKeyBindings); + Assert.False (keyPressed); + + Assert.True (view.OnKeyDownContinued); + Assert.False (view.OnInvokingKeyBindingsContinued); + Assert.False (view.OnKeyPressedContinued); + } + + + [Fact] + public void KeyPressed_Handled_True_Stops_Processing () + { + bool keyDown = false; + bool invokingKeyBindings = false; + bool keyPressed = false; + + var view = new OnKeyTestView (); + Assert.True (view.CanFocus); + view.CancelVirtualMethods = false; + + view.KeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyDown); + Assert.False (view.OnKeyDownContinued); + e.Handled = false; + keyDown = true; + }; + view.InvokingKeyBindings += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnInvokingKeyBindingsContinued); + e.Handled = false; + invokingKeyBindings = true; + }; + view.ProcessKeyDown += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyPressed); + Assert.False (view.OnKeyPressedContinued); + e.Handled = true; + keyPressed = true; + }; + + view.NewKeyDownEvent (new Key (KeyCode.A)); + Assert.True (keyDown); + Assert.True (invokingKeyBindings); + Assert.True (keyPressed); + + Assert.True (view.OnKeyDownContinued); + Assert.True (view.OnInvokingKeyBindingsContinued); + Assert.False (view.OnKeyPressedContinued); + } + + + [Fact] + public void KeyUp_Handled_True_Stops_Processing () + { + bool keyUp = false; + + var view = new OnKeyTestView (); + Assert.True (view.CanFocus); + view.CancelVirtualMethods = false; + + view.KeyUp += (s, e) => { + Assert.Equal (KeyCode.A, e.KeyCode); + Assert.False (keyUp); + Assert.False (view.OnKeyPressedContinued); + e.Handled = true; + keyUp = true; + }; + + view.NewKeyUpEvent (new Key (KeyCode.A)); + Assert.True (keyUp); + + Assert.False (view.OnKeyUpContinued); + Assert.False (view.OnKeyDownContinued); + Assert.False (view.OnInvokingKeyBindingsContinued); + Assert.False (view.OnKeyPressedContinued); + } + + /// + /// A view that overrides the OnKey* methods so we can test that they are called. + /// + public class OnKeyTestView : View { + public bool CancelVirtualMethods { set; private get; } + + public OnKeyTestView () => CanFocus = true; + + public override string Text { get; set; } + + public bool OnKeyDownContinued { get; set; } + + public bool OnInvokingKeyBindingsContinued { get; set; } + + public bool OnKeyPressedContinued { get; set; } + + public bool OnKeyUpContinued { get; set; } + + public override bool OnKeyDown (Key keyEvent) + { + if (base.OnKeyDown (keyEvent)) { + return true; + } + + OnKeyDownContinued = true; + return CancelVirtualMethods; + } + + public override bool? OnInvokingKeyBindings (Key keyEvent) + { + bool? handled = base.OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + return true; + } + + OnInvokingKeyBindingsContinued = true; + return CancelVirtualMethods; + } + + public override bool OnProcessKeyDown (Key keyEvent) + { + if (base.OnProcessKeyDown (keyEvent)) { + return true; + } + + OnKeyPressedContinued = true; + return CancelVirtualMethods; + } + + public override bool OnKeyUp (Key keyEvent) + { + if (base.OnKeyUp (keyEvent)) { + return true; + } + + OnKeyUpContinued = true; + return CancelVirtualMethods; + } + } + + [Theory] + [InlineData (true, false, false)] + [InlineData (true, true, false)] + [InlineData (true, true, true)] + public void Events_Are_Called_With_Only_Key_Modifiers (bool shift, bool alt, bool control) + { + bool keyDown = false; + bool keyPressed = false; + bool keyUp = false; + + var view = new OnKeyTestView (); + view.CancelVirtualMethods = false; + + view.KeyDown += (s, e) => { + Assert.Equal (KeyCode.Null, e.KeyCode & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask); + Assert.Equal (shift, e.IsShift); + Assert.Equal (alt, e.IsAlt); + Assert.Equal (control, e.IsCtrl); + Assert.False (keyDown); + Assert.False (view.OnKeyDownContinued); + keyDown = true; + }; + view.ProcessKeyDown += (s, e) => { + keyPressed = true; + }; + view.KeyUp += (s, e) => { + Assert.Equal (KeyCode.Null, e.KeyCode & ~KeyCode.CtrlMask & ~KeyCode.AltMask & ~KeyCode.ShiftMask); + Assert.Equal (shift, e.IsShift); + Assert.Equal (alt, e.IsAlt); + Assert.Equal (control, e.IsCtrl); + Assert.False (keyUp); + Assert.False (view.OnKeyUpContinued); + keyUp = true; + }; + + //view.ProcessKeyDownEvent (new (Key.Null | (shift ? Key.ShiftMask : 0) | (alt ? Key.AltMask : 0) | (control ? Key.CtrlMask : 0))); + //Assert.True (keyDown); + //Assert.True (view.OnKeyDownWasCalled); + //Assert.True (view.OnProcessKeyDownWasCalled); + + view.NewKeyDownEvent (new Key (KeyCode.Null | (shift ? KeyCode.ShiftMask : 0) | (alt ? KeyCode.AltMask : 0) | (control ? KeyCode.CtrlMask : 0))); + Assert.True (keyPressed); + Assert.True (view.OnKeyDownContinued); + Assert.True (view.OnKeyPressedContinued); + + view.NewKeyUpEvent (new Key (KeyCode.Null | (shift ? KeyCode.ShiftMask : 0) | (alt ? KeyCode.AltMask : 0) | (control ? KeyCode.CtrlMask : 0))); + Assert.True (keyUp); + Assert.True (view.OnKeyUpContinued); + } + + /// + /// This tests that when a new key down event is sent to the view + /// the view will fire the 3 key-down related events: KeyDown, InvokingKeyBindings, and ProcessKeyDown. + /// Note that KeyUp is independent. + /// + [Fact] + public void AllViews_KeyDown_All_EventsFire () + { + foreach (var view in TestHelpers.GetAllViews ()) { + if (view == null) { + _output.WriteLine ($"ERROR: null view from {nameof (TestHelpers.GetAllViews)}"); + continue; + } + _output.WriteLine ($"Testing {view.GetType ().Name}"); + + bool keyDown = false; + view.KeyDown += (s, a) => { + a.Handled = false; // don't handle it so the other events are called + keyDown = true; + }; + + bool invokingKeyBindings = false; + view.InvokingKeyBindings += (s, a) => { + a.Handled = false; // don't handle it so the other events are called + invokingKeyBindings = true; + }; + + bool keyDownProcessed = false; + view.ProcessKeyDown += (s, a) => { + a.Handled = true; + keyDownProcessed = true; + }; + + Assert.True (view.NewKeyDownEvent (Key.A)); // this will be true because the ProcessKeyDown event handled it + Assert.True (keyDown); + Assert.True (invokingKeyBindings); + Assert.True (keyDownProcessed); + view.Dispose (); + } + } + + /// + /// This tests that when a new key up event is sent to the view + /// the view will fire the 1 key-up related event: KeyUp + /// + [Fact] + public void AllViews_KeyUp_All_EventsFire () + { + foreach (var view in TestHelpers.GetAllViews ()) { + if (view == null) { + _output.WriteLine ($"ERROR: null view from {nameof (TestHelpers.GetAllViews)}"); + continue; + } + _output.WriteLine ($"Testing {view.GetType ().Name}"); + + bool keyUp = false; + view.KeyUp += (s, a) => { + a.Handled = true; + keyUp = true; + }; + + Assert.True (view.NewKeyUpEvent (Key.A)); // this will be true because the KeyUp event handled it + Assert.True (keyUp); + view.Dispose (); + } + + } +} \ No newline at end of file diff --git a/UnitTests/View/KeyboardTests.cs b/UnitTests/View/KeyboardTests.cs deleted file mode 100644 index a9557294b7..0000000000 --- a/UnitTests/View/KeyboardTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System; -using Xunit; -using Xunit.Abstractions; -using System.Text; - -// Alias Console to MockConsole so we don't accidentally use Console -using Console = Terminal.Gui.FakeConsole; - -namespace Terminal.Gui.ViewTests { - public class KeyboardTests { - readonly ITestOutputHelper output; - - public KeyboardTests (ITestOutputHelper output) - { - this.output = output; - } - - [Fact] - public void KeyPress_Handled_To_True_Prevents_Changes () - { - Application.Init (new FakeDriver ()); - - Console.MockKeyPresses.Push (new ConsoleKeyInfo ('N', ConsoleKey.N, false, false, false)); - - var top = Application.Top; - - var text = new TextField (""); - text.KeyPressed += (s, e) => { - e.Handled = true; - Assert.True (e.Handled); - Assert.Equal (Key.N, e.KeyEvent.Key); - }; - top.Add (text); - - Application.Iteration += (s, a) => { - Console.MockKeyPresses.Push (new ConsoleKeyInfo ('N', ConsoleKey.N, false, false, false)); - Assert.Equal ("", text.Text); - - Application.RequestStop (); - }; - - Application.Run (); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); - } - - - [Fact, AutoInitShutdown] - public void KeyDown_And_KeyUp_Events_Must_Called_Before_OnKeyDown_And_OnKeyUp () - { - var keyDown = false; - var keyPress = false; - var keyUp = false; - - var view = new DerivedView (); - view.KeyDown += (s, e) => { - Assert.Equal (Key.a, e.KeyEvent.Key); - Assert.False (keyDown); - Assert.False (view.IsKeyDown); - e.Handled = true; - keyDown = true; - }; - view.KeyPressed += (s, e) => { - Assert.Equal (Key.a, e.KeyEvent.Key); - Assert.False (keyPress); - Assert.False (view.IsKeyPress); - e.Handled = true; - keyPress = true; - }; - view.KeyUp += (s, e) => { - Assert.Equal (Key.a, e.KeyEvent.Key); - Assert.False (keyUp); - Assert.False (view.IsKeyUp); - e.Handled = true; - keyUp = true; - }; - - Application.Top.Add (view); - - Console.MockKeyPresses.Push (new ConsoleKeyInfo ('a', ConsoleKey.A, false, false, false)); - - Application.Iteration += (s, a) => Application.RequestStop (); - - Assert.True (view.CanFocus); - - Application.Run (); - Application.Shutdown (); - - Assert.True (keyDown); - Assert.True (keyPress); - Assert.True (keyUp); - Assert.False (view.IsKeyDown); - Assert.False (view.IsKeyPress); - Assert.False (view.IsKeyUp); - } - - public class DerivedView : View { - public DerivedView () - { - CanFocus = true; - } - - public bool IsKeyDown { get; set; } - public bool IsKeyPress { get; set; } - public bool IsKeyUp { get; set; } - public override string Text { get; set; } - - public override bool OnKeyDown (KeyEvent keyEvent) - { - IsKeyDown = true; - return true; - } - - public override bool ProcessKey (KeyEvent keyEvent) - { - IsKeyPress = true; - return true; - } - - public override bool OnKeyUp (KeyEvent keyEvent) - { - IsKeyUp = true; - return true; - } - } - - [Theory, AutoInitShutdown] - [InlineData (true, false, false)] - [InlineData (true, true, false)] - [InlineData (true, true, true)] - public void KeyDown_And_KeyUp_Events_With_Only_Key_Modifiers (bool shift, bool alt, bool control) - { - var keyDown = false; - var keyPress = false; - var keyUp = false; - - var view = new DerivedView (); - view.KeyDown += (s, e) => { - Assert.Equal (-1, e.KeyEvent.KeyValue); - Assert.Equal (shift, e.KeyEvent.IsShift); - Assert.Equal (alt, e.KeyEvent.IsAlt); - Assert.Equal (control, e.KeyEvent.IsCtrl); - Assert.False (keyDown); - Assert.False (view.IsKeyDown); - keyDown = true; - }; - view.KeyPressed += (s, e) => { - keyPress = true; - }; - view.KeyUp += (s, e) => { - Assert.Equal (-1, e.KeyEvent.KeyValue); - Assert.Equal (shift, e.KeyEvent.IsShift); - Assert.Equal (alt, e.KeyEvent.IsAlt); - Assert.Equal (control, e.KeyEvent.IsCtrl); - Assert.False (keyUp); - Assert.False (view.IsKeyUp); - keyUp = true; - }; - - Application.Top.Add (view); - - Console.MockKeyPresses.Push (new ConsoleKeyInfo ('\0', 0, shift, alt, control)); - - Application.Iteration += (s, a) => Application.RequestStop (); - - Assert.True (view.CanFocus); - - Application.Run (); - Application.Shutdown (); - - Assert.True (keyDown); - Assert.False (keyPress); - Assert.True (keyUp); - Assert.True (view.IsKeyDown); - Assert.False (view.IsKeyPress); - Assert.True (view.IsKeyUp); - } - - } -} diff --git a/UnitTests/View/Layout/DimTests.cs b/UnitTests/View/Layout/DimTests.cs index c70b66d458..3569347443 100644 --- a/UnitTests/View/Layout/DimTests.cs +++ b/UnitTests/View/Layout/DimTests.cs @@ -688,7 +688,7 @@ public void Dim_Add_Operator () var count = 0; field.KeyDown += (s, k) => { - if (k.KeyEvent.Key == Key.Enter) { + if (k.KeyCode == KeyCode.Enter) { field.Text = $"Label {count}"; var label = new Label (field.Text) { X = 0, Y = view.Bounds.Height, Width = 20 }; view.Add (label); @@ -703,7 +703,7 @@ public void Dim_Add_Operator () }; Application.Iteration += (s, a) => { - while (count < 20) field.OnKeyDown (new KeyEvent (Key.Enter, new KeyModifiers ())); + while (count < 20) field.NewKeyDownEvent (new (KeyCode.Enter)); Application.RequestStop (); }; @@ -1050,7 +1050,7 @@ public void Dim_Add_Operator_With_Text () var listLabels = new List