From dcb3b359adb66426c3c038322bdb16475e4af3b5 Mon Sep 17 00:00:00 2001 From: Tig Date: Sat, 16 Dec 2023 12:04:23 -0700 Subject: [PATCH] Fixes #2926 - Refactor KeyEvent and KeyEventEventArgs to simplify (#2927) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adds basic MainLoop unit tests * Remove WinChange action from Curses * Remove WinChange action from Curses * Remove ProcessInput action from Windows MainLoop * Simplified MainLoop/ConsoleDriver by making MainLoop internal and moving impt fns to Application * Modernized Terminal resize events * Modernized Terminal resize events * Removed un used property * for _isWindowsTerminal devenv->wininit; not sure what changed * Modernized mouse/keyboard events (Action->EventHandler) * Updated OnMouseEvent API docs * Using WT_SESSION to detect WT * removes hacky GetParentProcess * Updates to fix #2634 (clear last line) * removes hacky GetParentProcess2 * Addressed mac resize issue * Addressed mac resize issue * Removes ConsoleDriver.PrepareToRun, has Init return MainLoop * Removes unneeded Attribute methods * Removed GetProcesssName * Removed GetProcesssName * Refactored KeyEvent and KeyEventEventArgs into a single class * Revert "Refactored KeyEvent and KeyEventEventArgs into a single class" This reverts commit 88a00658dbcb53306d56af1b766594c0eea10b2c. * Fixed key repeat issue; reverted stupidity on 1049/1047 confusion * Updated CSI API Docs * merge * Rearranged Event.cs to Keyboard.cs and Mouse.cs * Renamed KeyEventEventArgs KeyEventArgs * temp renamed KeyEvent OldKeyEvent * Merged KeyEvent into KeyEventArgs * Renamed Application.ProcessKey members * Renamed Application.ProcessKey members * Renamed Application.ProcessKey members * Added Responder.KeyPressed * Removed unused references * Fixed arg naming * InvokeKeybindings->InvokeKeyBindings * InvokeKeybindings->InvokeKeyBindings * Fixed unit tests fail * More progress on refactoring key input; still broken and probably wrong * Moved OnKeyPressed out of Responder and made ProcessKeyPrssed non-virtual * Updated API docs * Moved key handling from Responder to View * Updated API docs * Updated HotKey API docs * Updated shortcut API docs * Fixed responder unit tests * Removed Shortcut from View as it is not used * Removed unneeded OnHotKey override from Button * Fixed BackTab logic * Button now uses Key Bindings exclusively * Button now uses Key Bindings exclusively * Updated keyboard.md docs * Fixed unit tests to account for Toplevel handling default button * Added View.InvokeCommand API * Modernized RadioGroup * Removed ColdKey * Modernized (partially) StatusBar * Worked around FileDialog issue with Ctrl-F * Fixed driver unit test; view must be focused to reciev key pressed * Application code cleanup * Start on updaing menu * Menu now mostly works * Menu Select refinement * Fixed known menu bugs! * Enabled HotKey to cause focus- experimental * Fixes #3022 & adds unit test to prove it * Actually Fixes #3022 & adds unit test to prove it * Working through hotkey issues * Misc fixes * removed hot/cold key stuff from Keys scenario * Fixed scenarios * Simplified shortcut string handling * Modernized Checkbox * Modernized TileView * Updated API docs * Updated API docs * attempting to publish v2 docs * Revert "attempting to publish v2 docs" This reverts commit 59dcec111b63121ca34f890d76728f40e81412b3. * Playing with api docs * Removed Key.BackTab * Removed Caps/Scroll/Numlock * Partial removal of keymodifiers - unit tests pass * Partial removal of keymodifiers - broke netdriver somewhere * WindowsDriver & added KeyEventArgsTests * Fixing menu shortcut/hotkeys - broke Menu.cs into separate files * Fixed MenuBar! * Finished modernizing Menu/MenuBar * Removed Key.a-z. Broke lots of stuff * checkout@v4 * progress on key mapping and formatting * VK tests are still failing * Fixed some unit tests * Added Hotkey and Keybinding unit tests * fixed unit test * All unit tests pass again... * Fixed broken unit tests * KeyEventArgs.KeyValue -> AsRune * Fixed bugs. Still some broken * Added KeyEventArgs.IsAlpha. Added KeyEventArgs.cast ops. Fixed bugs. Unit tests pass * Fixed WindowsDriver * Oops. * Refactoring based on bdisp's help. Not complete! * removed calling into subviews from OnKeyBindings * removed calling into subviews from OnKeyBindings * Improved View KeyEvent unit tests * More hotkey unit tests * BIg change - Got rid of KeyPress w/in Application/Drivers * Unit tests now pass again * Refreshed API docs * Better HotKey logic. More progress. Getting close. * Fixed handling of shifted chars like ö * Minor code cleanup * Minor code cleanup2 * Why is build Action failing? * Why is build Action failing?? * upgraded to .net8 to try to fix weird CI/CD build errors * upgraded to .net8 to try to fix weird CI/CD build errors2 * Disabling TextViewTests to diagnose build errors * reenable TextViewTests to diagnose build errors * Arrrrrrg * Merged v2_develop * Fixed uppercase accented keys in WindowsDriver * Fixed key binding api docs * Experimental impl of CommandScope.SubViews for MenuBar * Removed dead code from application.cs * Removed dead code from application.cs * Removed dead code from ConsoleDriver.cs * Cleaned up some key binding stuff * Disabled Alt to activate menu for now * Updated label commands * Fixed menu bugs. Upgraded menu unit tests * Fixed unit tests * Working on NetDriver * fixed netdriver * Fixed issues called out by @bdisp CR * fixed CursesDriver * added todo to netdriver * Cherry picked treeview test fix 1b415e5 * Fix NetDriver. * CommandScope->KeyBindingScope * Address some tznind feedback * Refactored KeyBindings big time! * Added key consts to KeyEventArgs and renamed Key to ConsoleDriverKey * Fixed some API docs * Moved ConsoleDriverKey to ConsoleDriver.cs * Renamed Key->ConsoleDriverKey * Renamed Key->ConsoleDriverKey * Renamed Key->ConsoleDriverKey * renamed file I forgot to rename before * Updated name and API docs of KeyEventArgs.isAlpha * Fixed issues with OnKeyUp not doing the right thing. * Fixed MainLoop.Running never being used * Fixed MainLoop.Running never being used - unit tests * Claned up BUGBUG comments * Disabled a unit test to see why ci/cd tests are failing * Removed defunct commented code * Removed more defunct commented code * Re-eanbled unit test; jsut removing one test case... * Disabled more... * Renambed Global->Applicaton and updated scope API docs * Disabled more unit tests... * Removed dead code * Disabled more unit tests...2 * Disabled more unit tests...3 * Renambed Global->Applicaton and updated scope API docs 2 * Added more KeyBinding scope tests * Added more KeyBinding scope tests2 * ConsoleDriverKey too long. Key too ambiguous. Settled on KeyCode. (Partialy because eventually I want to intro a class named Key). * KeyEventArgs improvements. cast to Rune must be explicit as it's lossy * Fixed warnings * Renamed KeyEventArgs to Key... progress on fixing broken stuff that resulted * Fix ConsoleKeyMapping bugs. * Fix NetDriver issue from converting a lower case to a upper case. * Started migration to Key from KeyCode - e.g. made HotKeys all consistent. * Fixed build warnings * Added key defns to Key * KeyBindings now uses Key vs. KeyCode * Verified by tweaking UICatalog * Fixed treeview test ... again * Renamed ProcessKeyDown/Up to NewKeyDown/Up and OnKeyPressed to OnProcessKeyDown to make things more clear * Added test AllViews_KeyDown_All_EventsFire unit tests and fixed a few Views that were wrong * fixed stupid KeyUp event bug * If key not handled, return false for datefield * dotnet test --no-restore --verbosity diag * dotnet test --blame * run tests on windows * Fix TestVKPacket unit test and move it to ConsoleKeyMappingTests.cs file. * Remove unnecessary commented code. * Tweaked unit tests and removed Key.BareKey * Fixed little details and updated api docs * updated api docs * AddKeyBindingsForHotKey: KeyCode->Key * Cleaned up more old KeyCode usages. Added TODOs --------- Co-authored-by: BDisp --- .editorconfig | 150 +- .github/workflows/api-docs.yml | 2 +- .github/workflows/dotnet-core.yml | 8 +- .github/workflows/publish.yml | 4 +- CONTRIBUTING.md | 2 +- Example/Example.csproj | 2 +- ReactiveExample/LoginViewModel.cs | 2 +- ReactiveExample/README.md | 2 +- ReactiveExample/ReactiveExample.csproj | 6 +- Terminal.Gui/Application.cs | 356 +- Terminal.Gui/Clipboard/Clipboard.cs | 2 - .../Configuration/KeyCodeJsonConverter.cs | 126 + .../Configuration/KeyJsonConverter.cs | 141 +- Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs | 480 +- .../ConsoleDrivers/ConsoleKeyMapping.cs | 601 + .../CursesDriver/CursesDriver.cs | 313 +- .../ConsoleDrivers/EscSeqUtils/EscSeqUtils.cs | 2 +- .../ConsoleDrivers/FakeDriver/FakeConsole.cs | 37 +- .../ConsoleDrivers/FakeDriver/FakeDriver.cs | 179 +- .../ConsoleDrivers/FakeDriver/FakeMainLoop.cs | 4 +- Terminal.Gui/ConsoleDrivers/NetDriver.cs | 147 +- Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 506 +- Terminal.Gui/Drawing/Thickness.cs | 1 + Terminal.Gui/Input/Command.cs | 799 +- Terminal.Gui/Input/ConsoleKeyMapping.cs | 560 - Terminal.Gui/Input/Event.cs | 837 - Terminal.Gui/Input/Key.cs | 976 ++ Terminal.Gui/Input/KeyBinding.cs | 286 + Terminal.Gui/Input/KeyChangedEventArgs.cs | 49 +- Terminal.Gui/Input/KeyEventEventArgs.cs | 24 - Terminal.Gui/Input/Mouse.cs | 185 + Terminal.Gui/Input/Responder.cs | 428 +- Terminal.Gui/Input/ShortcutHelper.cs | 402 +- Terminal.Gui/MainLoop.cs | 4 +- Terminal.Gui/Resources/config.json | 15 +- Terminal.Gui/Terminal.Gui.csproj | 7 +- .../Text/Autocomplete/AppendAutocomplete.cs | 12 +- .../Text/Autocomplete/AutocompleteBase.cs | 11 +- .../Text/Autocomplete/IAutocomplete.cs | 13 +- .../Text/Autocomplete/PopupAutocomplete.cs | 22 +- Terminal.Gui/Text/CollectionNavigatorBase.cs | 8 +- Terminal.Gui/Text/TextFormatter.cs | 34 +- Terminal.Gui/Text/ViewLayout.cs | 4 +- Terminal.Gui/View/View.cs | 35 +- Terminal.Gui/View/ViewDrawing.cs | 3 + Terminal.Gui/View/ViewEventArgs.cs | 2 +- Terminal.Gui/View/ViewKeyboard.cs | 1119 +- Terminal.Gui/View/ViewText.cs | 5 - Terminal.Gui/Views/Button.cs | 495 +- Terminal.Gui/Views/CheckBox.cs | 434 +- Terminal.Gui/Views/ColorPicker.cs | 18 +- Terminal.Gui/Views/ComboBox.cs | 30 +- Terminal.Gui/Views/ContextMenu.cs | 234 - Terminal.Gui/Views/DateField.cs | 809 +- Terminal.Gui/Views/Dialog.cs | 9 +- Terminal.Gui/Views/FileDialog.cs | 127 +- Terminal.Gui/Views/GraphView/Annotations.cs | 2 +- Terminal.Gui/Views/GraphView/GraphView.cs | 24 +- Terminal.Gui/Views/HexView.cs | 1272 +- Terminal.Gui/Views/Label.cs | 40 +- Terminal.Gui/Views/ListView.cs | 40 +- Terminal.Gui/Views/Menu.cs | 2215 --- Terminal.Gui/Views/Menu/ContextMenu.cs | 242 + Terminal.Gui/Views/Menu/Menu.cs | 1029 ++ Terminal.Gui/Views/Menu/MenuBar.cs | 1467 ++ Terminal.Gui/Views/Menu/MenuEventArgs.cs | 97 + Terminal.Gui/Views/MenuEventArgs.cs | 98 - Terminal.Gui/Views/RadioGroup.cs | 735 +- Terminal.Gui/Views/ScrollView.cs | 34 +- Terminal.Gui/Views/Slider.cs | 54 +- Terminal.Gui/Views/StatusBar.cs | 533 +- Terminal.Gui/Views/TabView.cs | 20 +- .../TableView/CheckBoxTableSourceWrapper.cs | 2 +- .../CheckBoxTableSourceWrapperByObject.cs | 123 +- Terminal.Gui/Views/TableView/ColumnStyle.cs | 2 +- Terminal.Gui/Views/TableView/TableStyle.cs | 2 +- Terminal.Gui/Views/TableView/TableView.cs | 92 +- .../Views/TableView/TreeTableSource.cs | 10 +- Terminal.Gui/Views/TextField.cs | 400 +- Terminal.Gui/Views/TextValidateField.cs | 29 +- Terminal.Gui/Views/TextView.cs | 218 +- Terminal.Gui/Views/TileView.cs | 163 +- Terminal.Gui/Views/TimeField.cs | 52 +- Terminal.Gui/Views/Toplevel.cs | 137 +- Terminal.Gui/Views/TreeView/Branch.cs | 2 + Terminal.Gui/Views/TreeView/TreeView.cs | 66 +- Terminal.Gui/Views/Wizard/Wizard.cs | 946 +- UICatalog/KeyBindingsDialog.cs | 18 +- UICatalog/Properties/launchSettings.json | 4 + UICatalog/Scenarios/ASCIICustomButton.cs | 20 +- UICatalog/Scenarios/AllViewsTester.cs | 4 +- .../Scenarios/BackgroundWorkerCollection.cs | 14 +- UICatalog/Scenarios/Buttons.cs | 540 +- UICatalog/Scenarios/CharacterMap.cs | 225 +- .../Scenarios/CollectionNavigatorTester.cs | 2 +- UICatalog/Scenarios/ConfigurationEditor.cs | 6 +- UICatalog/Scenarios/ContextMenus.cs | 242 +- UICatalog/Scenarios/CsvEditor.cs | 10 +- UICatalog/Scenarios/DynamicMenuBar.cs | 29 +- UICatalog/Scenarios/DynamicStatusBar.cs | 1107 +- UICatalog/Scenarios/Editor.cs | 70 +- UICatalog/Scenarios/Generic.cs | 71 +- UICatalog/Scenarios/GraphViewExample.cs | 2 +- UICatalog/Scenarios/HexEditor.cs | 6 +- UICatalog/Scenarios/InteractiveTree.cs | 12 +- UICatalog/Scenarios/Keys.cs | 288 +- UICatalog/Scenarios/LineDrawing.cs | 14 +- UICatalog/Scenarios/ListColumns.cs | 14 +- UICatalog/Scenarios/MenuBarScenario.cs | 217 + UICatalog/Scenarios/Notepad.cs | 85 +- UICatalog/Scenarios/SendKeys.cs | 14 +- UICatalog/Scenarios/SingleBackgroundWorker.cs | 14 +- UICatalog/Scenarios/Snake.cs | 12 +- UICatalog/Scenarios/TableEditor.cs | 14 +- UICatalog/Scenarios/Text.cs | 12 +- .../Scenarios/TextViewAutocompletePopup.cs | 4 +- UICatalog/Scenarios/TreeViewFileSystem.cs | 6 +- UICatalog/Scenarios/Unicode.cs | 4 +- UICatalog/Scenarios/VkeyPacketSimulator.cs | 91 +- UICatalog/UICatalog.cs | 1510 +- UICatalog/UICatalog.csproj | 5 +- UnitTests/Application/ApplicationTests.cs | 316 +- UnitTests/Application/KeyboardTests.cs | 396 + UnitTests/Application/MainLoopTests.cs | 8 +- UnitTests/Clipboard/ClipboardTests.cs | 282 +- .../Configuration/ConfigurationMangerTests.cs | 69 +- UnitTests/Configuration/JsonConverterTests.cs | 568 +- UnitTests/Configuration/SettingsScopeTests.cs | 32 +- .../ConsoleDrivers/ConsoleDriverTests.cs | 20 +- .../ConsoleDrivers/ConsoleKeyMappingTests.cs | 218 + UnitTests/ConsoleDrivers/KeyCodeTests.cs | 181 + UnitTests/ConsoleDrivers/KeyTests.cs | 348 - UnitTests/Dialogs/DialogTests.cs | 8 +- UnitTests/Drawing/ThicknessTests.cs | 4 - UnitTests/FileServices/FileDialogTests.cs | 19 +- UnitTests/Input/KeyBindingTests.cs | 288 + UnitTests/Input/KeyTests.cs | 325 + UnitTests/Input/ResponderTests.cs | 214 +- UnitTests/TestHelpers.cs | 97 +- UnitTests/Text/AutocompleteTests.cs | 36 +- UnitTests/Text/CollectionNavigatorTests.cs | 592 +- UnitTests/Text/TextFormatterTests.cs | 118 +- UnitTests/UICatalog/ScenarioTests.cs | 22 +- UnitTests/UnitTests.csproj | 8 +- UnitTests/View/HotKeyTests.cs | 307 + UnitTests/View/KeyboardEventTests.cs | 458 + UnitTests/View/KeyboardTests.cs | 181 - UnitTests/View/Layout/DimTests.cs | 20 +- UnitTests/View/Layout/PosTests.cs | 15 +- UnitTests/View/NavigationTests.cs | 80 +- UnitTests/View/ViewKeyBindingTests.cs | 148 + UnitTests/View/ViewTests.cs | 14 +- UnitTests/Views/AllViewsTests.cs | 319 +- UnitTests/Views/AppendAutocompleteTests.cs | 400 +- UnitTests/Views/ButtonTests.cs | 107 +- UnitTests/Views/CheckBoxTests.cs | 53 +- UnitTests/Views/ColorPickerTests.cs | 131 +- UnitTests/Views/ComboBoxTests.cs | 128 +- UnitTests/Views/ContextMenuTests.cs | 46 +- UnitTests/Views/DateFieldTests.cs | 27 +- UnitTests/Views/HexViewTests.cs | 114 +- UnitTests/Views/LabelTests.cs | 12 +- UnitTests/Views/ListViewTests.cs | 42 +- UnitTests/Views/MenuBarTests.cs | 2758 +++ UnitTests/Views/MenuTests.cs | 2812 +--- UnitTests/Views/OverlappedTests.cs | 2 + UnitTests/Views/RadioGroupTests.cs | 333 +- UnitTests/Views/RuneCellTests.cs | 3 + UnitTests/Views/ScrollViewTests.cs | 130 +- UnitTests/Views/StatusBarTests.cs | 28 +- UnitTests/Views/TableViewTests.cs | 213 +- UnitTests/Views/TextFieldTests.cs | 3213 ++-- UnitTests/Views/TextValidateFieldTests.cs | 114 +- UnitTests/Views/TextViewTests.cs | 13819 ++++++++-------- UnitTests/Views/TileViewTests.cs | 2748 +-- UnitTests/Views/TimeFieldTests.cs | 27 +- UnitTests/Views/ToplevelTests.cs | 2423 +-- UnitTests/Views/TreeTableSourceTests.cs | 16 +- UnitTests/Views/TreeViewTests.cs | 19 +- UnitTests/Views/WindowTests.cs | 9 +- docfx/docs/keyboard.md | 161 +- global.json | 2 +- 182 files changed, 32405 insertions(+), 29097 deletions(-) create mode 100644 Terminal.Gui/Configuration/KeyCodeJsonConverter.cs create mode 100644 Terminal.Gui/ConsoleDrivers/ConsoleKeyMapping.cs delete mode 100644 Terminal.Gui/Input/ConsoleKeyMapping.cs delete mode 100644 Terminal.Gui/Input/Event.cs create mode 100644 Terminal.Gui/Input/Key.cs create mode 100644 Terminal.Gui/Input/KeyBinding.cs delete mode 100644 Terminal.Gui/Input/KeyEventEventArgs.cs create mode 100644 Terminal.Gui/Input/Mouse.cs delete mode 100644 Terminal.Gui/Views/ContextMenu.cs delete mode 100644 Terminal.Gui/Views/Menu.cs create mode 100644 Terminal.Gui/Views/Menu/ContextMenu.cs create mode 100644 Terminal.Gui/Views/Menu/Menu.cs create mode 100644 Terminal.Gui/Views/Menu/MenuBar.cs create mode 100644 Terminal.Gui/Views/Menu/MenuEventArgs.cs delete mode 100644 Terminal.Gui/Views/MenuEventArgs.cs create mode 100644 UICatalog/Scenarios/MenuBarScenario.cs create mode 100644 UnitTests/Application/KeyboardTests.cs create mode 100644 UnitTests/ConsoleDrivers/ConsoleKeyMappingTests.cs create mode 100644 UnitTests/ConsoleDrivers/KeyCodeTests.cs delete mode 100644 UnitTests/ConsoleDrivers/KeyTests.cs create mode 100644 UnitTests/Input/KeyBindingTests.cs create mode 100644 UnitTests/Input/KeyTests.cs create mode 100644 UnitTests/View/HotKeyTests.cs create mode 100644 UnitTests/View/KeyboardEventTests.cs delete mode 100644 UnitTests/View/KeyboardTests.cs create mode 100644 UnitTests/View/ViewKeyBindingTests.cs create mode 100644 UnitTests/Views/MenuBarTests.cs diff --git a/.editorconfig b/.editorconfig index f2899c92a..c8b73b2d8 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 4e91d239f..ed4eca8cb 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 39bc0906e..f1afbb057 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 db34f23bb..6eda0c8c3 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 3c7851b85..32f409dbd 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 845bf6d6a..4bb8cc0bc 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 619e4c802..edd7b63dd 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 61f86cfa5..f5dbf705b 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 a9269f8bf..b3c0c5558 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 368552192..c57b6619c 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application.cs @@ -8,6 +8,7 @@ using System.IO; 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 @@ public static partial class Application { // 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 @@ public static partial class Application { // 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 @@ public static partial class Application { 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 @@ public static partial class Application { _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 @@ public static partial class Application { public static void Shutdown () { ResetState (); - ConfigurationManager.PrintJsonErrors (); + PrintJsonErrors (); } // Encapsulate all setting of initial state for Application; Having @@ -228,13 +226,18 @@ public static partial class Application { 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 @@ public static partial class Application { // (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 partial class Application { 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 partial class Application { 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 partial class Application { } 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 partial class Application { /// 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 partial class Application { } } 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 partial class Application { /// 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 partial class Application { /// 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 partial class Application { } /// - /// 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 partial class Application { // 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 static partial class Application { } /// - /// 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 partial class Application { 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 partial class Application { /// 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 partial class Application { MainLoop.RunIteration (); Iteration?.Invoke (null, new IterationEventArgs ()); - EnsureModalOrVisibleAlwaysOnTop (state.Toplevel); if (state.Toplevel != Current) { OverlappedTop?.OnDeactivate (state.Toplevel); @@ -663,7 +659,7 @@ public static partial class Application { 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 partial class Application { } } 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 partial class Application { 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 partial class Application { /// 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 partial class Application { 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 partial class Application { 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 partial class Application { // 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 partial class Application { 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 partial class Application { 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 @@ public static partial class Application { 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 @@ public static partial class Application { 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 @@ public static partial class Application { 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 @@ public static partial class Application { 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 @@ public static partial class Application { } 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 partial class Application { Refresh (); return true; } - #endregion Toplevel handling #region Mouse handling @@ -1176,9 +1170,9 @@ public static partial class Application { } 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 @@ public static partial class Application { #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 @@ public static partial class Application { } } - 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 @@ public static partial class Application { } } - 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 partial class Application { } } } + static void OnQuitKeyChanged (KeyChangedEventArgs e) { // Duplicate the list so if it changes during enumeration we're safe @@ -1368,115 +1362,119 @@ public static partial class Application { } /// - /// Event fired after a key has been pressed and released. - /// Set to to suppress the event. - /// - /// - /// All drivers support firing the event. Some drivers (Curses) - /// do not support firing the and events. - /// - public static event EventHandler KeyPressed; - - /// - /// Called after a key has been pressed and released. Fires the event. + /// Event fired when the user presses a key. Fired by . /// - /// Called for new KeyPressed events before any processing is performed or - /// views evaluate. Use for global key handling and/or debugging. + /// Set to to indicate the key was handled and + /// to prevent additional processing. /// /// - /// + /// + /// All drivers support firing the event. Some drivers (Curses) + /// do not support firing the and events. + /// + /// Fired after and before . + /// + /// + public static event EventHandler KeyDown; + + /// + /// 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. + /// Called by the when the user releases a key. + /// Fires the event + /// then calls on all top level views. + /// Called after . /// + /// + /// Can be used to simulate key press events. + /// /// - public static void OnKeyDown (KeyEventEventArgs a) + /// if the key was handled. + public static bool OnKeyUp (Key a) { - KeyDown?.Invoke (null, a); - var chain = _topLevels.ToList (); - foreach (var topLevel in chain) { - if (topLevel.OnKeyDown (a.KeyEvent)) - return; - if (topLevel.Modal) - break; + if (!_initialized) { + return true; } - } - /// - /// Event fired when a key is released. - /// - /// - /// All drivers support firing the event. Some drivers (Curses) - /// do not support firing the and events. - /// - public static event EventHandler KeyUp; - - /// - /// Called when a key is released. Fires the event. - /// - /// - public static void OnKeyUp (KeyEventEventArgs a) - { KeyUp?.Invoke (null, a); - var chain = _topLevels.ToList (); - foreach (var topLevel in chain) { - if (topLevel.OnKeyUp (a.KeyEvent)) - return; - if (topLevel.Modal) - break; + 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 47de47ab6..802f7e113 100644 --- a/Terminal.Gui/Clipboard/Clipboard.cs +++ b/Terminal.Gui/Clipboard/Clipboard.cs @@ -58,8 +58,6 @@ public static class Clipboard { 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 000000000..052c4c4b3 --- /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 3474c16d6..f292055b3 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 } - }; +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; + } - List modifiers = new List (); + if (reader.TokenType == JsonTokenType.PropertyName) { + string propertyName = reader.GetString (); + reader.Read (); - 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 == Key.Unknown || key == Key.Null) { - throw new JsonException ($"The value \"{reader.GetString ()}\" is not a valid Key."); - } - - } 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 0a92e0274..31d448a96 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 abstract class ConsoleDriver { #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 000000000..3c9d00da2 --- /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 27b1b7e92..98181f069 100644 --- a/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/CursesDriver/CursesDriver.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; +using Terminal.Gui.ConsoleDrivers; using Unix.Terminal; namespace Terminal.Gui; @@ -343,103 +344,82 @@ internal class CursesDriver : ConsoleDriver { 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 class CursesDriver : ConsoleDriver { 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 class CursesDriver : ConsoleDriver { 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 class CursesDriver : ConsoleDriver { 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 class CursesDriver : ConsoleDriver { 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)))); + } 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)); } - // 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)); - //} } - 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 @@ internal class CursesDriver : ConsoleDriver { } 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 @@ internal class CursesDriver : ConsoleDriver { 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 @@ internal class CursesDriver : ConsoleDriver { 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 b90a7a05f..7468832c1 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 69b203276..4cec5cb01 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeConsole.cs @@ -5,6 +5,7 @@ using System; 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 class FakeConsole { 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 f42388da8..a2eef29a1 100644 --- a/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/FakeDriver/FakeDriver.cs @@ -5,11 +5,13 @@ using System; 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 @@ public class FakeDriver : ConsoleDriver { 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 class FakeDriver : ConsoleDriver { } #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 class FakeDriver : ConsoleDriver { // 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 @@ public class FakeDriver : ConsoleDriver { 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.KeyChar != (uint)key) { + return ConsoleKeyMapping.MapKeyModifiers (keyInfo, (KeyCode)keyInfo.KeyChar); } - if (keyInfo.Modifiers == ConsoleModifiers.Alt) { - return (Key)(((uint)Key.AltMask) | ((uint)Key.A + delta)); + 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)); } - 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); - } - } - 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 class FakeDriver : ConsoleDriver { 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 class FakeDriver : ConsoleDriver { 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 0279a27d9..4fb09326f 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 @@ internal class FakeMainLoop : IMainLoopDriver { 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 6b06057b3..5aaff3439 100644 --- a/Terminal.Gui/ConsoleDrivers/NetDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/NetDriver.cs @@ -10,6 +10,7 @@ using System.Runtime.InteropServices; 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 @@ internal class NetEvents : IDisposable { } 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 @@ internal class NetEvents : IDisposable { _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 @@ internal class NetDriver : ConsoleDriver { // 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 @@ internal class NetDriver : ConsoleDriver { 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 @@ internal class NetDriver : ConsoleDriver { 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; - } - if ((keyInfo.Modifiers & ConsoleModifiers.Alt) != 0) { - keyMod |= Key.AltMask; - _keyModifiers.Alt = 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; } - return keyMod != Key.Null ? keyMod | key : key; + // 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 (KeyCode)(uint)keyInfo.KeyChar; } volatile bool _winSizeChanging; @@ -1131,19 +1137,10 @@ internal class NetDriver : ConsoleDriver { 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 3e10564c1..b78d0d2dc 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -22,6 +22,8 @@ using System.Runtime.InteropServices; 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 @@ internal class WindowsConsole { 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 @@ internal class WindowsConsole { 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 @@ internal class WindowsDriver : ConsoleDriver { } #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 class WindowsDriver : ConsoleDriver { 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}"); + var keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); + Debug.WriteLine ($"event: {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); - if (map == (Key)0xffffffff) { - KeyEvent key = new KeyEvent (); - // Shift = VK_SHIFT = 0x10 - // Ctrl = VK_CONTROL = 0x11 - // Alt = VK_MENU = 0x12 + var map = MapKey (keyInfo); - if (inputEvent.KeyEvent.dwControlKeyState.HasFlag (WindowsConsole.ControlKeyState.CapslockOn)) { - inputEvent.KeyEvent.dwControlKeyState &= ~WindowsConsole.ControlKeyState.CapslockOn; - } - - 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; - } - - 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 (); + var keyPressedEventArgs = new Key (map); - //if (_keyDown == (Key)0xffffffff) { - // Avoid sending repeat keydowns - // _keyDown = map; - OnKeyDown (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); - //} - OnKeyPressed (new KeyEventEventArgs (new KeyEvent (map, _keyModifiers))); + // 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 @@ internal class WindowsDriver : ConsoleDriver { return mouseFlag; } - KeyModifiers _keyModifiers; - public WindowsConsole.ConsoleKeyInfoEx ToConsoleKeyInfoEx (WindowsConsole.KeyEventRecord keyEvent) { var state = keyEvent.dwControlKeyState; @@ -1287,33 +1411,12 @@ internal class WindowsDriver : ConsoleDriver { 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 @@ internal class WindowsDriver : ConsoleDriver { 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 5f1fdd103..080b10d17 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -108,6 +108,7 @@ namespace Terminal.Gui { /// 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 b6ce90623..ed02dbfe1 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 +// 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; -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, /// - /// Actions which can be performed by the application or bound to keys in a control. + /// Moves down one item (cell, line, etc...). /// - 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 - } + 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 8902c1bf0..000000000 --- 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 8a50c8675..000000000 --- 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 000000000..67e5e0980 --- /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 000000000..e9a4354cc --- /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 eb1b37e9a..459d07a46 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 { + public Key OldKey { get; } - /// - /// Gets the old that was set before the event. - /// Use to check for empty. - /// - 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 c7ab9e39a..000000000 --- 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 000000000..5ee6a75f8 --- /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 e390f4a0d..bdf8ec1ea 100644 --- a/Terminal.Gui/Input/Responder.cs +++ b/Terminal.Gui/Input/Responder.cs @@ -18,286 +18,182 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -namespace Terminal.Gui { +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 /// - /// Responder base class implemented by objects that want to participate on keyboard and mouse input. + /// For debug purposes to verify objects are being disposed properly /// - 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); - } + 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 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 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 respond to user interaction. + /// + public virtual bool Enabled { get; set; } = true; - /// - /// 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 and all its child controls are displayed. + /// + public virtual bool Visible { get; set; } = true; - // 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. - /// - /// + /// + /// 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; + } - public virtual bool ProcessHotKey (KeyEvent kb) - { + /// + /// 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; + } + + /// + /// 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 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 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; + } + + /// + /// 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; } + return m.GetBaseDefinition ().DeclaringType != m.DeclaringType; + } - /// - /// 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; - } - - /// - /// 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; - } - - /// - /// 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; - } - - /// - /// 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 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; - } - - /// - /// 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; - } - - /// - /// 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 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 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; - } - - /// - /// 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; - } - } - - /// - /// 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; - - foreach (var instance in Instances.Where (x => x.WasDisposed).ToList ()) { - Instances.Remove (instance); - } -#endif + 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); +#if DEBUG_IDISPOSABLE + WasDisposed = true; + + 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 e1b1a3bdb..4d02f4128 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; - } - - /// - /// 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"; - } - 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; - } - - /// - /// 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; - } - - Key key = Key.Null; - //var hasCtrl = false; - if (delimiter == null) { - 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; - } - } - } else { - key |= (Key)Enum.Parse (typeof (Key), 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; - } - - /// - /// 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; - } - - return true; - } - - /// - /// 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; - } - 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 false; - } + } + + /// + /// 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 ""; + } + + 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; + } + } + 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 KeyCode GetShortcutFromTag (string tag, Rune delimiter = default) + { + var sCut = tag; + if (string.IsNullOrEmpty (sCut)) { + return default; + } + + KeyCode key = KeyCode.Null; + //var hasCtrl = false; + if (delimiter == default) { + delimiter = MenuBar.ShortcutDelimiter; + } + + 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 |= (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 (KeyCode key, KeyCode first, KeyCode last) + { + for (uint i = (uint)first; i < (uint)last; i++) { + if ((key | (KeyCode)i) == key) { + return true; + } + } + return false; + } + + /// + /// 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; + } + + return true; + } + + /// + /// 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 78275887f..c266abbc2 100644 --- a/Terminal.Gui/MainLoop.cs +++ b/Terminal.Gui/MainLoop.cs @@ -10,7 +10,7 @@ using System.Collections.ObjectModel; 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 @@ namespace Terminal.Gui { /// /// 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 67afbf4ae..af2c93e19 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 a71ea78d7..a73a94923 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 9aba845ed..9f8392d67 100644 --- a/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/AppendAutocomplete.cs @@ -29,7 +29,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - 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 @@ namespace Terminal.Gui { 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 6be3e8da0..524c7a243 100644 --- a/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs +++ b/Terminal.Gui/Text/Autocomplete/AutocompleteBase.cs @@ -39,14 +39,17 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 ac31d3f9c..ca57dda53 100644 --- a/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/IAutocomplete.cs @@ -53,20 +53,23 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 925f5bc53..d78767a46 100644 --- a/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs +++ b/Terminal.Gui/Text/Autocomplete/PopupAutocomplete.cs @@ -152,7 +152,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 1d65116dc..64161ebca 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -203,15 +203,15 @@ namespace Terminal.Gui { } /// - /// 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 1b38430bb..f332b52a3 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -929,20 +929,20 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { TextAlignment _textAlignment; VerticalTextAlignment _textVerticalAlignment; TextDirection _textDirection; - Key _hotKey; + Key _hotKey = new Key (); int _hotKeyPos = -1; Size _size; private bool _autoSize; @@ -1225,17 +1229,17 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 4109758ab..faeb8e174 100644 --- a/Terminal.Gui/Text/ViewLayout.cs +++ b/Terminal.Gui/Text/ViewLayout.cs @@ -534,7 +534,7 @@ namespace Terminal.Gui { } /// - /// Removes the setting on this view. + /// Indicates that the view does not need to be laid out. /// protected void ClearLayoutNeeded () { @@ -947,7 +947,7 @@ namespace Terminal.Gui { /// 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 66799dc50..aa23cf47c 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 @@ namespace Terminal.Gui { TextFormatter.HotKeyChanged += TextFormatter_HotKeyChanged; TextDirection = direction; - _shortcutHelper = new ShortcutHelper (); CanFocus = false; TabIndex = -1; TabStop = false; @@ -231,6 +234,8 @@ namespace Terminal.Gui { 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 833691495..4e5ed8206 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -200,6 +200,9 @@ namespace Terminal.Gui { /// 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 576d5e6ac..e56759fe0 100644 --- a/Terminal.Gui/View/ViewEventArgs.cs +++ b/Terminal.Gui/View/ViewEventArgs.cs @@ -63,7 +63,7 @@ namespace Terminal.Gui { } /// - /// 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 421f94162..d1c4e034c 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; +namespace Terminal.Gui; +public partial class View { - /// - /// 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; - } - } - } - - /// - /// 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'); - } - } - set { - TextFormatter.HotKeySpecifier = value; - SetHotKey (); - } - } - - /// - /// 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; - } - } - } - - /// - /// 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 (); - } - } - } - - 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); - } - - void SetTabIndex () - { - var i = 0; - foreach (var v in SuperView._tabIndexes) { - if (v._tabIndex == -1) { - continue; - } - v._tabIndex = i; - i++; - } - } - - 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; - } - } - - int _oldTabIndex; - - /// - /// Invoked when a character key is pressed and occurs after the key up event. - /// - public event EventHandler KeyPressed; - - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } - - var args = new KeyEventEventArgs (keyEvent); - KeyPressed?.Invoke (this, args); - if (args.Handled) + void AddCommands () + { + // By default, the Default command is bound to the HotKey enabling focus + AddCommand (Command.Default, () => { + if (CanFocus) { + SetFocus (); return true; - if (Focused?.Enabled == true) { - Focused?.KeyPressed?.Invoke (this, args); - if (args.Handled) - return true; } - - 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]) { - - 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 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; - } - } - } - - 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)); - } - - if (KeyBindings.ContainsKey (key)) { - KeyBindings [key] = command; - } else { - KeyBindings.Add (key, command); - } - } - - /// - /// 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; - } - } - - /// - /// 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); - } - } - - /// - /// 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); - } - } - - /// - /// Returns all commands that are supported by this . - /// - /// - public IEnumerable GetSupportedCommands () - { - return CommandImplementations.Keys; - } - - /// - /// 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; - } - - /// - public override bool ProcessHotKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } - - 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; - - foreach (var view in _subviews) - if (view.Enabled && view.ProcessHotKey (keyEvent)) - return true; return false; - } + }); - /// - public override bool ProcessColdKey (KeyEvent keyEvent) - { - if (!Enabled) { - return false; + // 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)); } - - 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; - - foreach (var view in _subviews) - if (view.Enabled && view.ProcessColdKey (keyEvent)) - return true; - return false; - } - - /// - /// Invoked when a key is pressed. - /// - public event EventHandler KeyDown; - - /// - public override bool OnKeyDown (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } - - 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; - } - } - - return false; - } - - /// - /// Invoked when a key is released. - /// - public event EventHandler KeyUp; - - /// - public override bool OnKeyUp (KeyEvent keyEvent) - { - if (!Enabled) { - return false; - } - - 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) { - return true; - } - } - - return false; - } - - void SetHotKey () - { - if (TextFormatter == null) { - return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created"); - } - TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk); - if (_hotKey != hk) { - HotKey = hk; + if (AddKeyBindingsForHotKey (_hotKey, value)) { + // This will cause TextFormatter_HotKeyChanged to be called, firing HotKeyChanged + _hotKey = TextFormatter.HotKey = 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; + } + + var newKey = hotKey == KeyCode.Unknown ? KeyCode.Null : hotKey; + + 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})."); + } + + 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; + } + + // Remove base version + if (KeyBindings.TryGet (prevHotKey, out _)) { + KeyBindings.Remove (prevHotKey); + } + + // Remove the Alt version + if (KeyBindings.TryGet (prevHotKey.WithAlt, out _)) { + KeyBindings.Remove (prevHotKey.WithAlt); + } + + if (_hotKey.KeyCode is >= KeyCode.A and <= KeyCode.Z) { + // Remove the shift version + if (KeyBindings.TryGet (prevHotKey.WithShift, out _)) { + KeyBindings.Remove (prevHotKey.WithShift); + } + // Remove alt | shift version + if (KeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _)) { + KeyBindings.Remove (prevHotKey.WithShift.WithAlt); + } + } + + // 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 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; + } + + + /// + /// 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 (); + } + } + + 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; + } + } + + #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 (); + } + } + } + + 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); + } + + void SetTabIndex () + { + var i = 0; + foreach (var v in SuperView._tabIndexes) { + if (v._tabIndex == -1) { + continue; + } + v._tabIndex = i; + i++; + } + } + + 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; + } + } + + #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; + } + + // By default the KeyBindingScope is View + + if (Focused?.NewKeyDownEvent (keyEvent) == true) { + return true; + } + + // Before (fire the cancellable event) + if (OnKeyDown (keyEvent)) { + return true; + } + + // During (this is what can be cancelled) + var handled = OnInvokingKeyBindings (keyEvent); + if (handled != null && (bool)handled) { + return true; + } + + // 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; + } + + + return keyEvent.Handled; + } + + /// + /// 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; + } + + if (Focused?.NewKeyUpEvent (keyEvent) == true) { + return true; + } + + // Before (fire the cancellable event) + if (OnKeyUp (keyEvent)) { + 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). + + // 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; + } + + 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; + } + + // * 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 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; + } + 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})"); + } + + // 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 8a5b2dd52..649622bbb 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -48,11 +48,6 @@ namespace Terminal.Gui { /// 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 5cd1605dd..897ed3beb 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; + 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. - /// - 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. - /// - /// - /// 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) { } - /// - /// 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) { } + /// + /// 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); + } - /// - /// 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; + // 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 ('_'); + HotKeySpecifier = new Rune ('_'); - _leftBracket = CM.Glyphs.LeftBracket; - _rightBracket = CM.Glyphs.RightBracket; - _leftDefault = CM.Glyphs.LeftDefaultIndicator; - _rightDefault = CM.Glyphs.RightDefaultIndicator; + _leftBracket = CM.Glyphs.LeftBracket; + _rightBracket = CM.Glyphs.RightBracket; + _leftDefault = CM.Glyphs.LeftDefaultIndicator; + _rightDefault = CM.Glyphs.RightDefaultIndicator; - CanFocus = true; - AutoSize = true; - this.is_default = is_default; - Text = text ?? string.Empty; + 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 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 NoDecorations { get; set; } - - /// - /// - /// - 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 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; - } - - /// - /// 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; - } - } - } - base.PositionCursor (); - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - - return base.OnEnter (view); } } + + /// + /// + /// + public bool NoDecorations { get; set; } + + /// + /// + /// + 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}"; + } + } + } + + bool AcceptKey () + { + //if (!HasFocus) { + // SetFocus (); + //} + OnClicked (); + return true; + } + + /// + /// 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.IsValid && Text != "") { + for (int i = 0; i < TextFormatter.Text.GetRuneCount (); i++) { + if (TextFormatter.Text [i] == Text [0]) { + Move (i, 0); + return; + } + } + } + base.PositionCursor (); + } + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + + return base.OnEnter (view); + } } diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index e8eaf8088..81c48da60 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; + /// + /// 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; - /// - /// 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); + } - /// - /// Called when the property changes. Invokes the event. - /// - public virtual void OnToggled (ToggleEventArgs e) - { - Toggled?.Invoke (this, e); - } + /// + /// 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. - /// - 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 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. 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); + } - /// - /// 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); - } + // 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; - // 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 (); - OnResizeNeeded (); - - // Things this view knows how to do - AddCommand (Command.ToggleChecked, () => ToggleChecked ()); - - // Default keybindings for this view - AddKeyBinding ((Key)' ', Command.ToggleChecked); - AddKeyBinding (Key.Space, Command.ToggleChecked); - } - - /// - 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 - }; - } - - string GetFormatterText () - { - if (AutoSize || string.IsNullOrEmpty (Text) || Frame.Width <= 2) { - return Text; - } - return Text [..Math.Min (Frame.Width - 2, Text.GetRuneCount ())]; - } - - /// - /// The state of the - /// - public bool? Checked { - get => @checked; - set { - if (value == null && !AllowNullChecked) { - return; - } - @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; - } - } - - /// - public override void PositionCursor () - { - Move (0, 0); - } - - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - return base.ProcessKey (kb); - } - - /// - public override bool ProcessHotKey (KeyEvent kb) - { - if (kb.Key == (Key.AltMask | HotKey)) - return ToggleChecked (); - - return false; - } - - bool ToggleChecked () - { + // Things this view knows how to do + AddCommand (Command.ToggleChecked, () => ToggleChecked ()); + AddCommand (Command.Accept, () => { 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; + ToggleChecked (); + return true; + }); + + // Default keybindings for this view + KeyBindings.Add (Key.Space, Command.ToggleChecked); + } + + + /// + public override Key HotKey { + get => base.HotKey; + set { + if (value is null || value.KeyCode is KeyCode.Unknown) { + throw new ArgumentException (nameof (value)); } - OnToggled (new ToggleEventArgs (previousChecked, Checked)); - SetNeedsDisplay (); - return true; - } + var prev = base.HotKey; + if (prev != value) { + var v = value == KeyCode.Unknown ? Key.Empty: value; + base.HotKey = TextFormatter.HotKey = v; - /// - public override bool MouseEvent (MouseEvent me) - { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus) - return false; - - ToggleChecked (); - - return true; - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - - return base.OnEnter (view); + // 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); + } + } } } + + /// + 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 + }; + } + + string GetFormatterText () + { + if (AutoSize || string.IsNullOrEmpty (Text) || Frame.Width <= 2) { + return Text; + } + return Text [..Math.Min (Frame.Width - 2, Text.GetRuneCount ())]; + } + + /// + /// The state of the + /// + public bool? Checked { + get => @_checked; + set { + if (value == null && !AllowNullChecked) { + return; + } + @_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; + } + } + + /// + public override void PositionCursor () + { + Move (0, 0); + } + + 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; + } + + OnToggled (new ToggleEventArgs (previousChecked, Checked)); + SetNeedsDisplay (); + return true; + } + + /// + public override bool MouseEvent (MouseEvent me) + { + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) || !CanFocus) + return false; + + ToggleChecked (); + + return true; + } + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + + return base.OnEnter (view); + } } diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 1cb98190c..0c9b92da4 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -143,10 +143,10 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 17d4e8ecf..ddf24086d 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -343,16 +343,16 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 4f448cc87..000000000 --- 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 2da853331..56ed3d8b1 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -10,419 +10,424 @@ using System.Globalization; 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; + /// + /// The passed event arguments containing the old value, new value, and format string. + /// + public event EventHandler> DateChanged; - int fieldLen => isShort ? shortFieldLen : longFieldLen; - string format => isShort ? shortFormat : longFormat; + /// + /// 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); - /// - /// 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. + /// + public DateField () : this (DateTime.MinValue) { } - /// - /// 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 (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); + + 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; + } - /// - /// 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) - { - 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); - } - - 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; } - } - - string GetInvarianteFormat () - { - return $"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"); - } - return $" {lf}"; - } - - string GetShortFormat (string lf) - { - return lf.Replace ("yyyy", "yy"); - } - - /// - /// Gets or sets the date of the . - /// - /// - /// - public DateTime Date { - get { - return date; - } - 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); - } - } - } - - /// - /// 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 (); - } - } - - /// - 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 (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); - - if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime 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}"; - } 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}"; - } - } - 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 (); - } 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 (); - } - } - } - 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; - } - } - return idx; - } - - void IncCursorPosition () - { - if (CursorPosition == fieldLen) - return; - if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; - } - - void DecCursorPosition () - { - if (CursorPosition == 1) - return; - if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition--; - } - - void AdjCursorPosition () - { - if (Text [CursorPosition] == sepChar.ToCharArray () [0]) - CursorPosition++; - } - - /// - 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; - } - - bool MoveRight () - { - IncCursorPosition (); - return true; - } - - new bool MoveEnd () - { - CursorPosition = fieldLen; - return true; - } - - bool MoveLeft () - { - DecCursorPosition (); - return true; - } - - bool MoveHome () - { - // Home, C-A - CursorPosition = 1; - return true; - } - - /// - public override void DeleteCharLeft (bool useOldCursorPos = true) - { - if (ReadOnly) { - return; - } - - SetText ((Rune)'0'); - DecCursorPosition (); - return; - } - - /// - public override void DeleteCharRight () - { - if (ReadOnly) - return; - - SetText ((Rune)'0'); - 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; - } - - /// - /// Event firing method for the event. - /// - /// Event arguments - public virtual void OnDateChanged (DateTimeEventArgs args) - { - DateChanged?.Invoke (this, args); + } catch (Exception) { + Text = e.OldValue; } } + + 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"); + } + } + return $" {lf}"; + } + + string GetShortFormat (string lf) => lf.Replace ("yyyy", "yy"); + + /// + /// Gets or sets the date of the . + /// + /// + /// + public DateTime Date { + get => date; + set { + if (ReadOnly) { + return; + } + + 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; + } + 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); + } + + 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; + } + + 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 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}"; + } 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 { + date += $"{year,2:00}"; + } + } + 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 (); + } 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 { + date [2] = vals [i].Trim (); + } + } + } + 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; + } + } + return idx; + } + + void IncCursorPosition () + { + if (CursorPosition == fieldLen) { + return; + } + if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition++; + } + } + + void DecCursorPosition () + { + if (CursorPosition == 1) { + return; + } + if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition--; + } + } + + void AdjCursorPosition () + { + if (Text [CursorPosition] == sepChar.ToCharArray () [0]) { + CursorPosition++; + } + } + + bool MoveRight () + { + IncCursorPosition (); + return true; + } + + new bool MoveEnd () + { + CursorPosition = fieldLen; + return true; + } + + bool MoveLeft () + { + DecCursorPosition (); + return true; + } + + bool MoveHome () + { + // Home, C-A + CursorPosition = 1; + return true; + } + + /// + public override void DeleteCharLeft (bool useOldCursorPos = true) + { + if (ReadOnly) { + return; + } + + SetText ((Rune)'0'); + DecCursorPosition (); + return; + } + + /// + public override void DeleteCharRight () + { + if (ReadOnly) { + return; + } + + SetText ((Rune)'0'); + return; + } + + /// + public override bool MouseEvent (MouseEvent ev) + { + if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) { + return false; + } + if (!HasFocus) { + SetFocus (); + } + + 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 db7016394..5e6165c08 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -228,15 +228,16 @@ namespace Terminal.Gui { } } + // 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 95d637a66..9d64cae11 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -142,22 +142,23 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { }; 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { return; } - k.Handled = this.TreeView_KeyDown (k.KeyEvent); + k.Handled = this.TreeView_KeyDown (k); }; @@ -484,23 +486,26 @@ namespace Terminal.Gui { } - /// - 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 @@ namespace Terminal.Gui { } } - 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { Application.RequestStop (); } - private void NavigateIf (KeyEventEventArgs keyEvent, Key isKey, View to) + private bool NavigateIf (Key keyEvent, KeyCode isKey, View to) { - if (!keyEvent.Handled) { - - if (NavigateIf (keyEvent.KeyEvent, isKey, to)) { - keyEvent.Handled = true; - } - } - } - - private bool NavigateIf (KeyEvent keyEvent, Key isKey, View to) - { - if (keyEvent.Key == isKey) { + if (keyEvent.KeyCode == isKey) { to.FocusFirst (); if (to == tbPath) { @@ -956,28 +951,28 @@ namespace Terminal.Gui { } } - 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 1ef30c9f6..a2bbfc9cd 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 2d2310b1b..32f579b38 100644 --- a/Terminal.Gui/Views/GraphView/GraphView.cs +++ b/Terminal.Gui/Views/GraphView/GraphView.cs @@ -81,14 +81,14 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 b425e5f07..4e38d6a4b 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -10,627 +10,655 @@ using System.Collections.Generic; using System.IO; using System.Text; -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; - - private 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 - 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 (); - } - } - - 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; - - SetDisplayStart (value); - } - } - - const int displayWidth = 9; - const int bsize = 4; - int bpl; - private int bytesPerLine { - get => bpl; - set { - bpl = value; - OnPositionChanged (); - } - } - - /// - 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); - } - } - - // - // 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]; - } - - /// - 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 ? " " : "| "); - } - - 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 - SetAttribute (GetNormalColor ()); - - Driver.AddRune (c); - } - } - - void SetAttribute (Attribute attribute) - { - if (currentAttribute != attribute) { - currentAttribute = attribute; - Driver.SetAttribute (attribute); - } - } - } - - /// - public override void PositionCursor () - { - var delta = (int)(position - displayStart); - var line = delta / bytesPerLine; - var item = delta % bytesPerLine; - var block = item / bsize; - var 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; - - SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); - } - - bool MoveEndOfLine () - { - position = Math.Min ((position / bytesPerLine * bytesPerLine) + bytesPerLine - 1, source.Length); - SetNeedsDisplay (); - - return true; - } - - bool MoveStartOfLine () - { - position = position / bytesPerLine * bytesPerLine; - SetNeedsDisplay (); - - return true; - } - - bool MoveEnd () - { - position = source.Length; - if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { - SetDisplayStart (position); - SetNeedsDisplay (); - } else - RedisplayLine (position); - - return true; - } - - bool MoveHome () - { - DisplayStart = 0; - SetNeedsDisplay (); - - return 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; - if (position - 1 < DisplayStart) { - SetDisplayStart (displayStart - bytesPerLine); - SetNeedsDisplay (); - } else - RedisplayLine (position); - position--; - - return true; - } - - bool MoveRight () - { - 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; - } - - bool MoveUp (int bytes) - { - RedisplayLine (position); - if (position - bytes > -1) - position -= bytes; - if (position < DisplayStart) { - SetDisplayStart (DisplayStart - bytes); - SetNeedsDisplay (); - } else - RedisplayLine (position); - - 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))) { - var p = position; - while (p + bytesPerLine < source.Length) { - p += bytesPerLine; - } - position = p; - } - if (position >= (DisplayStart + bytesPerLine * Frame.Height)) { - SetDisplayStart (DisplayStart + bytes); - SetNeedsDisplay (); - } else - RedisplayLine (position); - - return true; - } - - /// - public override bool ProcessKey (KeyEvent keyEvent) - { - var result = InvokeKeybindings (keyEvent); - if (result != null) - return (bool)result; - - if (!AllowEdits) - return false; - - // Ignore control characters and other special keys - if (keyEvent.Key < Key.Space || keyEvent.Key > Key.CharMask) - return false; - - 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; - } - - /// - /// Method used to invoke the event passing the . - /// - /// The key value pair. - public virtual void OnEdited (HexViewEditEventArgs e) - { - Edited?.Invoke (this, e); - } - - /// - /// Method used to invoke the event passing the arguments. - /// - public virtual void OnPositionChanged () - { - PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); - } - - /// - 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 (!HasFocus) - SetFocus (); - - if (me.Flags == MouseFlags.WheeledDown) { - DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length); - return true; - } - - if (me.Flags == MouseFlags.WheeledUp) { - DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0); - 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 (); - - return true; - } - - /// - /// 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); - } - } - - /// - /// 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 (); - } - - /// - /// This method discards the edits made to the by resetting the - /// contents of the property. - /// - public void DiscardEdits () - { - edits = new SortedDictionary (); - } - - private CursorVisibility desiredCursorVisibility = CursorVisibility.Default; - - /// - /// 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); - - return base.OnEnter (view); +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); + } + + /// + /// 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 (); + } + } + + 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; + + SetDisplayStart (value); + } + } + + 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; + + // 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) + { + 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; + + 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.AddStr (offset >= n && !edited ? " " : string.Format ("{0:x2}", value)); + SetAttribute (GetNormalColor ()); + Driver.AddRune ((Rune)' '); + } + Driver.AddStr (block + 1 == nblocks ? " " : "| "); + } + + 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); + } + } + + void SetAttribute (Attribute attribute) + { + if (currentAttribute != attribute) { + currentAttribute = attribute; + Driver.SetAttribute (attribute); + } + } + } + + /// + 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) + { + int delta = (int)(pos - DisplayStart); + int line = delta / bytesPerLine; + + SetNeedsDisplay (new Rect (0, line, Frame.Width, 1)); + } + + bool MoveEndOfLine () + { + position = Math.Min (position / bytesPerLine * bytesPerLine + bytesPerLine - 1, source.Length); + SetNeedsDisplay (); + + return true; + } + + bool MoveStartOfLine () + { + position = position / bytesPerLine * bytesPerLine; + SetNeedsDisplay (); + + return true; + } + + bool MoveEnd () + { + position = source.Length; + if (position >= DisplayStart + bytesPerLine * Frame.Height) { + SetDisplayStart (position); + SetNeedsDisplay (); + } else { + RedisplayLine (position); + } + + return true; + } + + bool MoveHome () + { + DisplayStart = 0; + SetNeedsDisplay (); + + return 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; + } + if (position - 1 < DisplayStart) { + SetDisplayStart (displayStart - bytesPerLine); + SetNeedsDisplay (); + } else { + RedisplayLine (position); + } + position--; + + return true; + } + + bool MoveRight () + { + 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; + } + + bool MoveUp (int bytes) + { + RedisplayLine (position); + if (position - bytes > -1) { + position -= bytes; + } + if (position < DisplayStart) { + SetDisplayStart (DisplayStart - bytes); + SetNeedsDisplay (); + } else { + RedisplayLine (position); + } + + 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); + } + + return true; + } + + /// + public override bool OnProcessKeyDown (Key keyEvent) + { + if (!AllowEdits) { + return false; + } + + // 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; + } + + 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; + } + } + + /// + /// Method used to invoke the event passing the . + /// + /// The key value pair. + public virtual void OnEdited (HexViewEditEventArgs e) + { + Edited?.Invoke (this, e); + } + + /// + /// Method used to invoke the event passing the arguments. + /// + public virtual void OnPositionChanged () + { + PositionChanged?.Invoke (this, new HexViewEventArgs (Position, CursorPosition, BytesPerLine)); + } + + /// + 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 (!HasFocus) { + SetFocus (); + } + + if (me.Flags == MouseFlags.WheeledDown) { + DisplayStart = Math.Min (DisplayStart + bytesPerLine, source.Length); + return true; + } + + if (me.Flags == MouseFlags.WheeledUp) { + DisplayStart = Math.Max (DisplayStart - bytesPerLine, 0); + return true; + } + + 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.Button1DoubleClicked) { + leftSide = !leftSide; + if (leftSide) { + firstNibble = empty == 1; + } else { + firstNibble = true; + } + } + SetNeedsDisplay (); + + return true; + } + + /// + /// 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 { + int delta = (int)position; + int line = delta / bytesPerLine + 1; + int item = delta % bytesPerLine + 1; + + return new Point (item, line); + } + } + + /// + /// 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 (); + } + + /// + /// This method discards the edits made to the by resetting the + /// contents of the property. + /// + public void DiscardEdits () + { + edits = new SortedDictionary (); + } + + CursorVisibility desiredCursorVisibility = CursorVisibility.Default; + + /// + /// 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); + + 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 29b476fb7..40350e9c5 100644 --- a/Terminal.Gui/Views/Label.cs +++ b/Terminal.Gui/Views/Label.cs @@ -58,12 +58,31 @@ namespace Terminal.Gui { { 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 @@ namespace Terminal.Gui { 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 a5dd1ed7e..5e57efbe9 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -166,9 +166,9 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { return true; } } - return false; } diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs deleted file mode 100644 index da8b55023..000000000 --- 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 000000000..53983536b --- /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 000000000..34739612b --- /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 000000000..b74934812 --- /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 000000000..906c6050a --- /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 6f3e4c3d3..000000000 --- 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 c72fa9a29..8045e038f 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -3,414 +3,409 @@ using System; 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; + public RadioGroup () : this (radioLabels: new string [] { }) { } - /// - /// 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 () + { + SetInitialProperties (Rect.Empty, 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 () - { - SetInitalProperties (Rect.Empty, radioLabels, selected); + /// + /// 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); + } + + /// + /// 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; } - /// - /// 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); - } + _selected = selected; + Frame = rect; + CanFocus = true; - /// - /// 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) - { } + // 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; }); - void SetInitalProperties (Rect rect, string [] radioLabels, int selected) - { - if (radioLabels == null) { - this.radioLabels = new List (); - } else { - this.radioLabels = radioLabels.ToList (); - } + // 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); - this.selected = selected; - Frame = rect; - CanFocus = true; - HotKeySpecifier = new Rune ('_'); + LayoutStarted += RadioGroup_LayoutStarted; + } - // 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; }); + void RadioGroup_LayoutStarted (object sender, EventArgs e) + { + SetWidthHeight (_radioLabels); + } - // 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; - } - - private 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 (); - } - } - } - - 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); - } - - int width = 0; - - 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); - } - SelectedItem = 0; - cursor = 0; + /// + /// Gets or sets the for this . + /// + public DisplayModeLayout DisplayMode { + get { return _displayMode; } + set { + if (_displayMode != value) { + _displayMode = value; + SetWidthHeight (_radioLabels); 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)); - } + /// + /// 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 (); } } + } - /// - public override void OnDrawContent (Rect contentArea) - { - base.OnDrawContent (contentArea); + 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; - 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); - 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); - } + 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); } - /// - public override void PositionCursor () - { + int width = 0; + + 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. 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); + } + } + 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 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; + } + } + + } + 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 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 (); - } - } - - /// - /// Allow to invoke the after their creation. - /// - public void Refresh () - { - OnSelectedItemChanged (selected, -1); - } - - /// - /// 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)); - } - - /// - 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; + 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 ()); } } - i++; + Application.Driver.AddRune (rune); + Driver.SetAttribute (GetNormalColor ()); } - } - return false; - } - - /// - public override bool ProcessKey (KeyEvent kb) - { - var result = InvokeKeybindings (kb); - if (result != null) - return (bool)result; - - return base.ProcessKey (kb); - } - - void SelectItem () - { - SelectedItem = cursor; - } - - void MoveEnd () - { - cursor = Math.Max (radioLabels.Count - 1, 0); - } - - void MoveHome () - { - cursor = 0; - } - - void MoveDown () - { - if (cursor + 1 < radioLabels.Count) { - cursor++; - SetNeedsDisplay (); - } else if (cursor > 0) { - cursor = 0; - SetNeedsDisplay (); + } else { + DrawHotString (rl, HasFocus && i == _cursor, ColorScheme); } } + } - 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 (); - - 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; - - 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; - } - - /// - public override bool OnEnter (View view) - { - Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); - - return base.OnEnter (view); + /// + public override void PositionCursor () + { + switch (DisplayMode) { + case DisplayModeLayout.Vertical: + Move (0, _cursor); + break; + case DisplayModeLayout.Horizontal: + Move (_horizontal [_cursor].pos, 0); + break; } } /// - /// Used for choose the display mode of this + /// Invoked when the selected radio label has changed. /// - public enum DisplayModeLayout { - /// - /// Vertical mode display. It's the default. - /// - Vertical, - /// - /// Horizontal mode display. - /// - Horizontal + 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 (); + } + } + + /// + /// Allow to invoke the after their creation. + /// + public void Refresh () + { + OnSelectedItemChanged (_selected, -1); + } + + /// + /// 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 MoveEnd () + { + _cursor = Math.Max (_radioLabels.Count - 1, 0); + } + + void MoveHome () + { + _cursor = 0; + } + + 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 (); + } + } + + /// + 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; + + 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 (); + } + } + return true; + } + + /// + public override bool OnEnter (View view) + { + Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); + + return base.OnEnter (view); } } + +/// +/// Used for choose the display mode of this +/// +public enum DisplayModeLayout { + /// + /// Vertical mode display. It's the default. + /// + Vertical, + /// + /// Horizontal mode display. + /// + Horizontal +} diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index e4b8f3a1a..02b57510e 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -104,23 +104,23 @@ public class ScrollView : View { 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 class ScrollView : View { } /// - 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 52bc5c03e..2b161bc29 100644 --- a/Terminal.Gui/Views/Slider.cs +++ b/Terminal.Gui/Views/Slider.cs @@ -1403,48 +1403,34 @@ public class Slider : View { void SetKeyBindings () { if (_config._sliderOrientation == Orientation.Horizontal) { - AddKeyBinding (Key.CursorRight, Command.Right); - ClearKeyBinding (Key.CursorDown); - AddKeyBinding (Key.CursorLeft, Command.Left); - ClearKeyBinding (Key.CursorUp); + KeyBindings.Add (KeyCode.CursorRight, Command.Right); + KeyBindings.Remove (KeyCode.CursorDown); + KeyBindings.Add (KeyCode.CursorLeft, Command.Left); + KeyBindings.Remove (KeyCode.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 | 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 1d66b853d..4fdd0b6c9 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; - } - - /// - /// Gets the global shortcut to invoke the action on the menu. - /// - public Key Shortcut { get; set; } - - /// - /// Gets or sets the title. - /// - /// The title. - /// - /// The colour of the will be changed after each ~. - /// A set to `~F1~ Help` will render as *F1* using and - /// *Help* as . - /// - public string Title { get; set; } - - /// - /// 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 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; } - - /// - /// Returns if the status item is enabled. This method is a wrapper around . - /// - public bool IsEnabled () - { - return CanExecute == null ? true : CanExecute (); - } - - /// - /// Gets or sets arbitrary data for the status item. - /// - /// This property is not used internally. - public object Data { get; set; } - }; + /// + public string Title { 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. + /// Gets or sets the action to be invoked when the statusbar item is triggered /// - public class StatusBar : View { - /// - /// The items that compose the - /// - public StatusItem [] Items { get; set; } + /// Action to invoke. + public Action Action { get; set; } - /// - /// Initializes a new instance of the class. - /// - public StatusBar () : this (items: new StatusItem [] { }) { } + /// + /// 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; } - /// - /// 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 () - { - Items = items; - CanFocus = false; - ColorScheme = Colors.Menu; - X = 0; - Y = Pos.AnchorEnd (1); - Width = Dim.Fill (); - Height = 1; - } + /// + /// Returns if the status item is enabled. This method is a wrapper around . + /// + public bool IsEnabled () + { + return CanExecute?.Invoke () ?? true; + } - 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; - } + /// + /// 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 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); } - } - - Attribute ToggleScheme (Attribute scheme) - { - var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; - Driver.SetAttribute (result); - return result; - } - - Attribute DetermineColorSchemeFor (StatusItem item) - { - if (item != null) { - if (item.IsEnabled ()) { - return GetNormalColor (); - } - return ColorScheme.Disabled; + _items = value; + foreach (var item in _items) { + KeyBindings.Add ((KeyCode)item.Shortcut, KeyBindingScope.HotKey, Command.Accept); } - 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)' '); - } - - 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)' '); - } - } - } - - /// - 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 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); - } - break; - } - pos += GetItemTitleLength (Items [i].Title) + 3; - } - return true; - } - - int GetItemTitleLength (string title) - { - int len = 0; - foreach (var ch in title) { - if (ch == '~') - continue; - len++; - } - - return len; - } - - 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); - - 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 (); - } - - /// - /// 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; } } -} \ No newline at end of file + + /// + /// Initializes a new instance of the class. + /// + 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 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; + AddCommand (Command.Accept, InvokeItem); + } + + StatusItem _itemToInvoke; + bool? InvokeItem () + { + if (_itemToInvoke is { Action: not null }) { + _itemToInvoke.Action.Invoke (); + return true; + } + return false; + } + + /// + 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; + } + } + + } + return base.OnInvokingKeyBindings (keyEvent); + } + static Rune _shortcutDelimiter = (Rune)'='; + StatusItem [] _items = new StatusItem [] { }; + + /// + /// Gets or sets shortcut delimiter separator. The default is "-". + /// + public static Rune ShortcutDelimiter { + get => _shortcutDelimiter; + set { + if (_shortcutDelimiter != value) { + _shortcutDelimiter = value == default ? (Rune)'=' : value; + } + } + } + + Attribute ToggleScheme (Attribute scheme) + { + var result = scheme == ColorScheme.Normal ? ColorScheme.HotNormal : ColorScheme.Normal; + Driver.SetAttribute (result); + return result; + } + + Attribute DetermineColorSchemeFor (StatusItem item) + { + if (item != null) { + if (item.IsEnabled ()) { + return GetNormalColor (); + } + return ColorScheme.Disabled; + } + 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)' '); + } + + 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)' '); + } + } + } + + /// + 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); + } + break; + } + pos += GetItemTitleLength (Items [i].Title) + 3; + } + return true; + } + + int GetItemTitleLength (string title) + { + int len = 0; + foreach (var ch in title) { + if (ch == '~') + continue; + len++; + } + + return len; + } + + 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); + + 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 (); + } + + /// + /// 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; + } +} diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 86a558b82..9b8c80bca 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -123,10 +123,10 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 405727955..3e59a118e 100644 --- a/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs +++ b/Terminal.Gui/Views/TableView/CheckBoxTableSourceWrapper.cs @@ -29,7 +29,7 @@ namespace Terminal.Gui { 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 b6d811639..736fef46a 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); } - } - - /// - protected override void ClearAllToggles () - { - foreach (var e in toWrap.GetAllObjects()) { - setter (e, 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); + } + } } diff --git a/Terminal.Gui/Views/TableView/ColumnStyle.cs b/Terminal.Gui/Views/TableView/ColumnStyle.cs index 079cafae0..80e07ca2a 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 7fc3d6b99..35479ed97 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 f9a21a344..c04d1252b 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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); + 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); - 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); + 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); - AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll); - AddKeyBinding (CellActivationKey, Command.Accept); + KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.SelectAll); + KeyBindings.Add (CellActivationKey, Command.Accept); } /// @@ -758,36 +760,29 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { { 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 @@ namespace Terminal.Gui { 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 0b06691fd..599cc4a1e 100644 --- a/Terminal.Gui/Views/TableView/TreeTableSource.cs +++ b/Terminal.Gui/Views/TableView/TreeTableSource.cs @@ -38,7 +38,7 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T { _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 class TreeTableSource : IEnumerableTableSource, IDisposable where T /// public void Dispose () { - _tableView.KeyPressed -= Table_KeyPress; + _tableView.KeyDown -= Table_KeyPress; _tableView.MouseClick -= Table_MouseClick; _tree.Dispose (); } @@ -106,7 +106,7 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T 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 @@ public class TreeTableSource : IEnumerableTableSource, IDisposable where T 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 8f18fe933..f1e2a4375 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -13,7 +13,6 @@ using System.Threading; 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// Sets or gets the text held by the view. /// - /// - /// public new string Text { get { return StringExtensions.ToString (_text); @@ -315,8 +311,8 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// /// - 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; + _preTextChangedCursorPos = _cursorPosition; - // Give autocomplete first opportunity to respond to key presses - if (SelectedLength == 0 && Autocomplete.Suggestions.Count > 0 && Autocomplete.ProcessKey (kb)) { + // Ignore other control characters. + if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask)) { + return false; + } + + if (ReadOnly) { 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; - - // Ignore other control characters. - if (kb.Key < Key.Space || kb.Key > Key.CharMask) - return false; - - 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { _selectedStart = 0; MoveEndExtend (); - DeleteCharLeft (); + DeleteCharLeft (false); SetNeedsDisplay (); } @@ -1024,7 +1027,7 @@ namespace Terminal.Gui { } else { _selectedStart = value; } - PrepareSelection (_selectedStart, _point - _selectedStart); + PrepareSelection (_selectedStart, _cursorPosition - _selectedStart); } } @@ -1105,7 +1108,7 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } else if (_start > -1 && _length == 0) { _selectedText = null; } + SetNeedsDisplay (); } else if (_length > 0 || _selectedText != null) { ClearAllSelection (); } @@ -1199,8 +1203,8 @@ namespace Terminal.Gui { void SetSelectedStartSelectedLength () { - if (SelectedStart > -1 && _point < SelectedStart) { - _start = _point; + if (SelectedStart > -1 && _cursorPosition < SelectedStart) { + _start = _cursorPosition; } else { _start = SelectedStart; } @@ -1234,12 +1238,12 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// 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 f687a82a5..7f3f74d34 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -400,15 +400,15 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - 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 @@ namespace Terminal.Gui { CursorRight (); } - return true; + return false; } /// diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index cca066948..70205045d 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -1673,126 +1673,127 @@ namespace Terminal.Gui { }); // 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { { // 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 @@ namespace Terminal.Gui { _selectionStartColumn = nStartCol; _wrapNeeded = true; - SetNeedsDisplay(); - } + SetNeedsDisplay (); + } if (_currentCaller != null) throw new InvalidOperationException ($"WordWrap settings was changed after the {_currentCaller} call."); } @@ -3058,16 +3059,16 @@ namespace Terminal.Gui { { 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 @@ namespace Terminal.Gui { bool _shiftSelecting; /// - public override bool ProcessKey (KeyEvent kb) + 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); + } + + /// + public override bool OnProcessKeyDown (Key a) { if (!CanFocus) { return true; } - // 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; ResetColumnTrack (); - // Ignore control characters and other special keys - if (kb.Key < Key.Space || kb.Key > Key.CharMask) - return false; - InsertText (kb); + // Ignore control characters and other special keys + if (!a.IsKeyCodeAtoZ && (a.KeyCode < KeyCode.Space || a.KeyCode > KeyCode.CharMask)) { + return false; + } + + InsertText (a); DoNeededAction (); return true; @@ -3793,7 +3798,7 @@ namespace Terminal.Gui { if (!AllowsTab || _isReadOnly) { return ProcessMoveNextView (); } - InsertText (new KeyEvent ((Key)'\t', null)); + InsertText (new Key ((KeyCode)'\t')); DoNeededAction (); return true; } @@ -4320,11 +4325,12 @@ namespace Terminal.Gui { _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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } /// - 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 fcdc3d2a1..e32eef6c8 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } /// - /// 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { } 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 @@ namespace Terminal.Gui { { // 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { } // 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 @@ namespace Terminal.Gui { } // 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 @@ namespace Terminal.Gui { } // 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 ff0b7198b..173edd2ff 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -80,30 +80,30 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { } /// - 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 2d702ae88..210618d93 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { /// /// 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 @@ namespace Terminal.Gui { 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); + KeyBindings.Add ((KeyCode)Application.QuitKey, Command.QuitToplevel); - AddKeyBinding (Key.Tab, Command.NextView); + KeyBindings.Add (KeyCode.CursorRight, Command.NextView); + KeyBindings.Add (KeyCode.CursorDown, Command.NextView); + KeyBindings.Add (KeyCode.CursorLeft, Command.PreviousView); + KeyBindings.Add (KeyCode.CursorUp, Command.PreviousView); - AddKeyBinding (Key.CursorRight, Command.NextView); - AddKeyBinding (Key.F | Key.CtrlMask, Command.NextView); + 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); - AddKeyBinding (Key.CursorDown, Command.NextView); - AddKeyBinding (Key.I | Key.CtrlMask, Command.NextView); // Unix + 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 - 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); +#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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// /// /// - /// 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 @@ namespace Terminal.Gui { /// /// /// - /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { } } - /// - 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 83acdf745..cfcdaa209 100644 --- a/Terminal.Gui/Views/TreeView/Branch.cs +++ b/Terminal.Gui/Views/TreeView/Branch.cs @@ -195,6 +195,8 @@ namespace Terminal.Gui { 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 62beaee08..b5ca8f482 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// 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 @@ namespace Terminal.Gui { /// (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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { 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 @@ namespace Terminal.Gui { // 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 @@ namespace Terminal.Gui { } } } 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 8b8d0c89b..722a585c3 100644 --- a/Terminal.Gui/Views/Wizard/Wizard.cs +++ b/Terminal.Gui/Views/Wizard/Wizard.cs @@ -4,532 +4,524 @@ using System.Linq; 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 { + public Wizard () : base () + { - /// - /// 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; - // 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); - //// 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); - // 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); - nextfinishBtn = new Button (Strings.wzFinish) { AutoSize = true }; - nextfinishBtn.IsDefault = true; - AddButton (nextfinishBtn); + backBtn.Clicked += BackBtn_Clicked; + nextfinishBtn.Clicked += NextfinishBtn_Clicked; - 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 (); + 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; + } + + void Wizard_TitleChanged (object sender, TitleEventArgs e) + { + if (string.IsNullOrEmpty (wizardTitle)) { + wizardTitle = e.NewTitle; + } + } + + void Wizard_Loaded (object sender, EventArgs args) => CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null + + bool finishedPressed = false; + + void Wizard_Closing (object sender, ToplevelClosingEventArgs obj) + { + if (!finishedPressed) { + var args = new WizardButtonEventArgs (); + Cancelled?.Invoke (this, args); + } + } + + 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 (); } } + } - private void Wizard_Loaded (object sender, EventArgs args) - { - CurrentStep = GetFirstStep (); // gets the first step if CurrentStep == null - } - - private bool finishedPressed = false; - - private void Wizard_Closing (object sender, ToplevelClosingEventArgs obj) - { - if (!finishedPressed) { + /// + /// 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; + } - 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 (); - } - } + /// + /// 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 . - /// 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; - } - } - return base.ProcessKey (kb); - } - - /// - /// 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); - } - } - - /// - /// 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; - } - } - - // 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); - } - - /// - /// Returns the last enabled step in the Wizard - /// - /// The last enabled step - public WizardStep GetLastStep () - { - return steps.LastOrDefault (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 (); - } - - ///// - ///// 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); + // 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 first enabled step in the Wizard + /// + /// The last enabled step + public WizardStep GetFirstStep () => steps.FirstOrDefault (s => s.Enabled); + + /// + /// Returns the last enabled step in the Wizard + /// + /// The last enabled step + public WizardStep GetLastStep () => steps.LastOrDefault (s => s.Enabled); + + LinkedList steps = new (); + 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 => backBtn; + + 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 => nextfinishBtn; + + 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); + Add (newStep); + 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; + + /// + /// 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); + } + + /// + /// 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; } - /// - /// 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; + // Hide all but the new step + foreach (var step in steps) { + step.Visible = step == newStep; + step.ShowHide (); } - /// - /// 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; + var oldStep = currentStep; + currentStep = newStep; + + UpdateButtonsAndTitle (); + + // Set focus to the nav buttons + if (backBtn.HasFocus) { + backBtn.SetFocus (); + } else { + nextfinishBtn.SetFocus (); } - /// - /// 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 (OnStepChanged (oldStep, currentStep)) { + // For correctness we do this, but it's meaningless because there's nothing to cancel + return false; + } + + return true; + } + + void UpdateButtonsAndTitle () + { + if (CurrentStep == null) { + return; + } + + 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..."; + } + + 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); } - - // Hide all but the new step - foreach (WizardStep step in steps) { - step.Visible = (step == newStep); - step.ShowHide (); - } - - var oldStep = currentStep; - currentStep = newStep; - - UpdateButtonsAndTitle (); - - // Set focus to the nav buttons - if (backBtn.HasFocus) { - backBtn.SetFocus (); + if (base.Modal) { + ColorScheme = Colors.Dialog; + BorderStyle = LineStyle.Rounded; } else { - nextfinishBtn.SetFocus (); - } - - if (OnStepChanged (oldStep, currentStep)) { - // For correctness we do this, but it's meaningless because there's nothing to cancel - return false; - } - - return true; - } - - private void UpdateButtonsAndTitle () - { - if (CurrentStep == null) return; - - 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..."; - } - - SizeStep (CurrentStep); - - SetNeedsLayout (); - LayoutSubviews (); - Draw (); - } - - 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); - } - } - - /// - /// 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; + 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 538320f38..306e29539 100644 --- a/UICatalog/KeyBindingsDialog.cs +++ b/UICatalog/KeyBindingsDialog.cs @@ -8,8 +8,8 @@ using Terminal.Gui; 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 @@ namespace UICatalog { Dictionary knownViews = new Dictionary (); private object lockKnownViews = new object (); - private Dictionary keybindings; + private Dictionary keybindings; public ViewTracker (View top) { @@ -70,7 +70,7 @@ namespace UICatalog { Instance = new ViewTracker (Application.Top); } - internal void StartUsingNewKeyMap (Dictionary currentBindings) + internal void StartUsingNewKeyMap (Dictionary currentBindings) { lock (lockKnownViews) { @@ -109,8 +109,8 @@ namespace UICatalog { 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 @@ namespace UICatalog { 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 b06e2ce0b..4b3e5f027 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 eabde34c5..cb2250a1f 100644 --- a/UICatalog/Scenarios/ASCIICustomButton.cs +++ b/UICatalog/Scenarios/ASCIICustomButton.cs @@ -21,7 +21,7 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { }; } - 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 class CharacterMap : Scenario { }; 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 class CharacterMap : Scenario { //jumpList.Style.ShowVerticalHeaderLines = false; _categoryList.Style.AlwaysShowHeaders = true; - var isDescending = false; + bool isDescending = false; _categoryList.Table = CreateCategoryTable (0, isDescending); @@ -81,19 +87,19 @@ public class CharacterMap : Scenario { _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 class CharacterMap : Scenario { _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 class CharacterMap : Scenario { _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 class CharacterMap : Scenario { 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 @@ public class CharacterMap : Scenario { 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 @@ public class CharacterMap : Scenario { 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 @@ public class CharacterMap : Scenario { 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 @@ public class CharacterMap : Scenario { } 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { // 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 @@ class CharMap : ScrollView { /// 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { // 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { } } 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { var spinner = new SpinnerView () { X = Pos.Center (), Y = Pos.Center (), - Style = new SpinnerStyle.Aesthetic (), + Style = new Aesthetic () }; spinner.AutoSpin = true; @@ -640,11 +641,11 @@ class CharMap : ScrollView { 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 @@ class CharMap : ScrollView { // 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 @@ class CharMap : ScrollView { } 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 class UcdApiClient { } } - 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 993e993b8..ce1a8444f 100644 --- a/UICatalog/Scenarios/CollectionNavigatorTester.cs +++ b/UICatalog/Scenarios/CollectionNavigatorTester.cs @@ -95,7 +95,7 @@ namespace UICatalog.Scenarios { 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 9ff0115b8..2cc7a7327 100644 --- a/UICatalog/Scenarios/ConfigurationEditor.cs +++ b/UICatalog/Scenarios/ConfigurationEditor.cs @@ -49,11 +49,11 @@ namespace UICatalog.Scenarios { 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 c39cb248c..49ed6ec8c 100644 --- a/UICatalog/Scenarios/ContextMenus.cs +++ b/UICatalog/Scenarios/ContextMenus.cs @@ -3,97 +3,104 @@ using System.Globalization; 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; +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; + public override void Setup () + { + var text = "Context Menu"; + var width = 20; + KeyCode winContextMenuKey = KeyCode.Space | KeyCode.CtrlMask; - Win.Add (new Label ("Press 'Ctrl + Space' to open the Window context menu.") { - X = Pos.Center (), - Y = 1 - }); + 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); + _tfTopLeft = new TextField (text) { + Width = width + }; + Win.Add (_tfTopLeft); - _tfTopRight = new TextField (text) { - X = Pos.AnchorEnd (width), - Width = width - }; - Win.Add (_tfTopRight); + _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); + _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); + _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); + _tfBottomRight = new TextField (text) { + X = Pos.AnchorEnd (width), + Y = Pos.AnchorEnd (1), + Width = width + }; + Win.Add (_tfBottomRight); - Point mousePos = default; + 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; - - void ApplicationMouseEvent (object sender, MouseEventEventArgs a) - { - mousePos = new Point (a.MouseEvent.X, a.MouseEvent.Y); + Win.KeyDown += (s, e) => { + if (e.KeyCode == winContextMenuKey) { + ShowContextMenu (mousePos.X, mousePos.Y); + e.Handled = true; } + }; - Win.WantMousePositionReports = true; + Win.MouseClick += (s, e) => { + if (e.MouseEvent.Flags == _contextMenu.MouseFlags) { + ShowContextMenu (e.MouseEvent.X, e.MouseEvent.Y); + e.Handled = true; + } + }; - Application.Top.Closed += (s,e) => { - Thread.CurrentThread.CurrentUICulture = new CultureInfo ("en-US"); - Application.MouseEvent -= ApplicationMouseEvent; - }; + 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 @@ namespace UICatalog.Scenarios { }, 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 96ff667ff..57bb099ab 100644 --- a/UICatalog/Scenarios/CsvEditor.cs +++ b/UICatalog/Scenarios/CsvEditor.cs @@ -67,8 +67,8 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { tableView.SelectedCellChanged += OnSelectedCellChanged; tableView.CellActivated += EditCurrentCell; - tableView.KeyPressed += TableViewKeyPress; + tableView.KeyDown += TableViewKeyPress; SetupScrollBar (); } @@ -465,9 +465,9 @@ namespace UICatalog.Scenarios { } - 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 62a8fcb80..657cc8dc9 100644 --- a/UICatalog/Scenarios/DynamicMenuBar.cs +++ b/UICatalog/Scenarios/DynamicMenuBar.cs @@ -84,11 +84,11 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { } 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 e28092117..1acd55b0e 100644 --- a/UICatalog/Scenarios/DynamicStatusBar.cs +++ b/UICatalog/Scenarios/DynamicStatusBar.cs @@ -7,652 +7,649 @@ using System.Reflection; 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 () +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 DynamicStatusItemList () { } + + public DynamicStatusItemList (string title, StatusItem statusItem) { - Application.Init (); - Application.Top.Add (new DynamicStatusBarSample () { Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" }); + Title = title; + StatusItem = statusItem; } - public class DynamicStatusItemList { - public string Title { get; set; } - public StatusItem StatusItem { get; set; } + public override string ToString () => $"{Title}, {StatusItem}"; + } - public DynamicStatusItemList () { } + public class DynamicStatusItem { + public string title = "New"; + public string action = ""; + public string shortcut; - public DynamicStatusItemList (string title, StatusItem statusItem) - { - Title = title; - StatusItem = statusItem; - } + public DynamicStatusItem () { } - public override string ToString () => $"{Title}, {StatusItem}"; + public DynamicStatusItem (string title) + { + this.title = title; } - public class DynamicStatusItem { - public string title = "New"; - public string action = ""; - public string shortcut; - - public DynamicStatusItem () { } - - public DynamicStatusItem (string title) - { - this.title = title; - } - - public DynamicStatusItem (string title, string action, string shortcut = null) - { - this.title = title; - this.action = action; - this.shortcut = shortcut; - } + public DynamicStatusItem (string title, string action, string shortcut = null) + { + this.title = title; + this.action = action; + this.shortcut = shortcut; } + } - public class DynamicStatusBarSample : Window { - StatusBar _statusBar; - StatusItem _currentStatusItem; - int _currentSelectedStatusBar = -1; - StatusItem _currentEditStatusItem; - ListView _lstItems; + public class DynamicStatusBarSample : Window { + StatusBar _statusBar; + StatusItem _currentStatusItem; + int _currentSelectedStatusBar = -1; + StatusItem _currentEditStatusItem; + ListView _lstItems; - public DynamicStatusItemModel DataContext { get; set; } + public DynamicStatusItemModel DataContext { get; set; } - public DynamicStatusBarSample () : base () - { - DataContext = new DynamicStatusItemModel (); + public DynamicStatusBarSample () : base () + { + DataContext = new DynamicStatusItemModel (); - var _frmDelimiter = new FrameView ("Shortcut Delimiter:") { - X = Pos.Center (), - Y = 0, - Width = 25, - Height = 4 - }; + 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); + 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); + Add (_frmDelimiter); - var _frmStatusBar = new FrameView ("Items:") { - Y = 5, - Width = Dim.Percent (50), - Height = Dim.Fill (2) - }; + 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 _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 _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); + 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); + _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 _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 _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); + var _btnDown = new Button ("v") { + X = Pos.Right (_lstItems) + 2, + Y = Pos.Top (_btnUp) + 1 + }; + _frmStatusBar.Add (_btnDown); - Add (_frmStatusBar); + 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); + 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 (); - } + _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 _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); - } - }; - - _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; - } - - 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; - } - _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; - } - - _statusBar = new StatusBar (); - Add (_statusBar); - }; - - _btnRemoveStatusBar.Clicked += (s,e) => { - if (_statusBar == null) { - return; - } - - Remove (_statusBar); - _statusBar = null; - DataContext.Items = new List (); - _currentStatusItem = null; - _currentSelectedStatusBar = -1; - SetListViewSource (_currentStatusItem, true); - SetFrameDetails (null); - }; + 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 ustringConverter = new UStringValueConverter (); - var listWrapperConverter = new ListWrapperConverter (); + _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); + } + }; - var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter); - - void SetFrameDetails (StatusItem statusItem = null) - { - StatusItem newStatusItem; - - if (statusItem == null) { - newStatusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; - } else { - newStatusItem = statusItem; - } - - _currentEditStatusItem = newStatusItem; - _frmStatusBarDetails.EditStatusItem (newStatusItem); - var f = _btnOk.Enabled == _frmStatusBarDetails.Enabled; - if (!f) { - _btnOk.Enabled = _frmStatusBarDetails.Enabled; - _btnCancel.Enabled = _frmStatusBarDetails.Enabled; - } + _btnAdd.Clicked += (s, e) => { + if (StatusBar == null) { + MessageBox.ErrorQuery ("StatusBar Bar Error", "Must add a StatusBar first!", "Ok"); + _btnAddStatusBar.SetFocus (); + return; } - 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)); - } - } + var frameDetails = new DynamicStatusBarDetails (); + var item = frameDetails.EnterStatusItem (); + if (item == null) { + return; } - StatusItem CreateNewStatusBar (DynamicStatusItem item) - { - var newStatusItem = new StatusItem (ShortcutHelper.GetShortcutFromTag ( - item.shortcut, StatusBar.ShortcutDelimiter), - item.title, _frmStatusBarDetails.CreateAction (item)); + StatusItem newStatusItem = CreateNewStatusBar (item); + _currentSelectedStatusBar++; + _statusBar.AddItemAt (_currentSelectedStatusBar, newStatusItem); + DataContext.Items.Add (new DynamicStatusItemList (newStatusItem.Title, newStatusItem)); + _lstItems.MoveDown (); + SetFrameDetails (); + }; - return newStatusItem; - } - - 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)); + _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; } - DataContext.Items [index] = new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem); - SetFrameDetails (_currentEditStatusItem); + _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; } - //_frmStatusBarDetails.Initialized += (s, e) => _frmStatusBarDetails.Enabled = false; - } + _statusBar = new StatusBar (); + Add (_statusBar); + }; - public static string SetTitleText (string title, string shortcut) + _btnRemoveStatusBar.Clicked += (s, e) => { + if (_statusBar == null) { + return; + } + + Remove (_statusBar); + _statusBar = null; + DataContext.Items = new List (); + _currentStatusItem = null; + _currentSelectedStatusBar = -1; + SetListViewSource (_currentStatusItem, true); + SetFrameDetails (null); + }; + + SetFrameDetails (); + + var ustringConverter = new UStringValueConverter (); + var listWrapperConverter = new ListWrapperConverter (); + + var lstItems = new Binding (this, "Items", _lstItems, "Source", listWrapperConverter); + + void SetFrameDetails (StatusItem statusItem = null) { - var txt = title; - var split = title.Split ('~'); - if (split.Length > 1) { - txt = split [2].Trim (); ; - } - if (string.IsNullOrEmpty (shortcut)) { - return txt; + StatusItem newStatusItem; + + if (statusItem == null) { + newStatusItem = DataContext.Items.Count > 0 ? DataContext.Items [_lstItems.SelectedItem].StatusItem : null; + } else { + newStatusItem = statusItem; } - return $"~{shortcut}~ {txt}"; + _currentEditStatusItem = newStatusItem; + _frmStatusBarDetails.EditStatusItem (newStatusItem); + var f = _btnOk.Enabled == _frmStatusBarDetails.Enabled; + if (!f) { + _btnOk.Enabled = _frmStatusBarDetails.Enabled; + _btnCancel.Enabled = _frmStatusBarDetails.Enabled; + } } + + 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)); + } + } + } + + StatusItem CreateNewStatusBar (DynamicStatusItem item) + { + var newStatusItem = new StatusItem (ShortcutHelper.GetShortcutFromTag ( + item.shortcut, StatusBar.ShortcutDelimiter), + item.title, _frmStatusBarDetails.CreateAction (item)); + + return newStatusItem; + } + + 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)); + } + DataContext.Items [index] = new DynamicStatusItemList (_currentEditStatusItem.Title, _currentEditStatusItem); + SetFrameDetails (_currentEditStatusItem); + } + + //_frmStatusBarDetails.Initialized += (s, e) => _frmStatusBarDetails.Enabled = false; } - 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.") - { - _statusItem = statusItem; + 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; } - public DynamicStatusBarDetails (string title) : base (title) - { - var _lblTitle = new Label ("Title:") { - Y = 1 - }; - Add (_lblTitle); + return $"~{shortcut}~ {txt}"; + } + } - _txtTitle = new TextField () { - X = Pos.Right (_lblTitle) + 4, - Y = Pos.Top (_lblTitle), - Width = Dim.Fill () - }; - Add (_txtTitle); + public class DynamicStatusBarDetails : FrameView { + public StatusItem _statusItem; + public TextField _txtTitle; + public TextView _txtAction; + public TextField _txtShortcut; - var _lblAction = new Label ("Action:") { - X = Pos.Left (_lblTitle), - Y = Pos.Bottom (_lblTitle) + 1 - }; - Add (_lblAction); + public DynamicStatusBarDetails (StatusItem statusItem = null) : this (statusItem == null ? "Adding New StatusBar Item." : "Editing StatusBar Item.") + { + _statusItem = statusItem; + } - _txtAction = new TextView () { - X = Pos.Left (_txtTitle), - Y = Pos.Top (_lblAction), - Width = Dim.Fill (), - Height = 5 - }; - Add (_txtAction); + public DynamicStatusBarDetails (string title) : base (title) + { + var _lblTitle = new Label ("Title:") { + Y = 1 + }; + Add (_lblTitle); - var _lblShortcut = new Label ("Shortcut:") { - X = Pos.Left (_lblTitle), - Y = Pos.Bottom (_txtAction) + 1 - }; - Add (_lblShortcut); + _txtTitle = new TextField () { + X = Pos.Right (_lblTitle) + 4, + Y = Pos.Top (_lblTitle), + Width = Dim.Fill () + }; + Add (_txtTitle); - _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; - } + var _lblAction = new Label ("Action:") { + X = Pos.Left (_lblTitle), + Y = Pos.Bottom (_lblTitle) + 1 + }; + Add (_lblAction); - var k = ShortcutHelper.GetModifiersKey (e.KeyEvent); - if (CheckShortcut (k, true)) { - e.Handled = true; - } - }; + _txtAction = new TextView () { + X = Pos.Left (_txtTitle), + Y = Pos.Top (_lblAction), + Width = Dim.Fill (), + Height = 5 + }; + Add (_txtAction); - bool ProcessKey (KeyEvent ev) - { - switch (ev.Key) { - case Key.CursorUp: - case Key.CursorDown: - case Key.Tab: - case Key.BackTab: - return false; - } + var _lblShortcut = new Label ("Shortcut:") { + X = Pos.Left (_lblTitle), + Y = Pos.Bottom (_txtAction) + 1 + }; + Add (_lblShortcut); - return true; + _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 CheckShortcut (Key k, bool pre) - { - var m = _statusItem != null ? _statusItem : new StatusItem (k, "", null); - if (pre && !ShortcutHelper.PreShortcutValidation (k)) { + if (CheckShortcut (e.KeyCode, true)) { + e.Handled = true; + } + }; + + bool ProcessKey (Key ev) + { + switch (ev.KeyCode) { + case KeyCode.CursorUp: + case KeyCode.CursorDown: + case KeyCode.Tab: + case KeyCode.Tab | KeyCode.ShiftMask: + return false; + } + + 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 _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 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 = ShortcutHelper.GetShortcutTag (statusItem.Shortcut, StatusBar.ShortcutDelimiter) ?? ""; - } - - void CleanEditStatusItem () - { - _txtTitle.Text = ""; - _txtAction.Text = ""; + var _btnShortcut = new Button ("Clear Shortcut") { + X = Pos.X (_lblShortcut), + Y = Pos.Bottom (_txtShortcut) + 1 + }; + _btnShortcut.Clicked += (s, e) => { _txtShortcut.Text = ""; + }; + Add (_btnShortcut); + } + + public DynamicStatusItem EnterStatusItem () + { + var valid = false; + + if (_statusItem == null) { + var m = new DynamicStatusItem (); + _txtTitle.Text = m.title; + _txtAction.Text = m.action; + } else { + EditStatusItem (_statusItem); } - 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); + 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 (); } - return v == null || !(v is DynamicStatusItem item) ? string.Empty : item.action; - } + }; + 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." }; - public Action CreateAction (DynamicStatusItem item) - { - return new Action (() => MessageBox.ErrorQuery (item.title, item.action, "Ok")); + 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 class DynamicStatusItemModel : INotifyPropertyChanged { - public event PropertyChangedEventHandler PropertyChanged; + 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) ?? ""; + } - private string statusBar; - private List items; + void CleanEditStatusItem () + { + _txtTitle.Text = ""; + _txtAction.Text = ""; + _txtShortcut.Text = ""; + } - public string StatusBar { - get => statusBar; - set { - if (value != statusBar) { - statusBar = value; - PropertyChanged?.Invoke (this, new PropertyChangedEventArgs (GetPropertyName ())); + 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; + } + + public Action CreateAction (DynamicStatusItem item) + { + return new Action (() => MessageBox.ErrorQuery (item.title, item.action, "Ok")); + } + } + + public class DynamicStatusItemModel : INotifyPropertyChanged { + public event PropertyChangedEventHandler PropertyChanged; + + private string statusBar; + private List items; + + 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 DynamicStatusItemModel () + { + Items = new List (); + } + + public string GetPropertyName ([CallerMemberName] string propertyName = null) + { + return propertyName; + } + } + + public interface IValueConverter { + object Convert (object value, object parameter = null); + } + + public class Binding { + public View Target { get; private set; } + public View Source { get; private set; } + + public string SourcePropertyName { get; private set; } + public string TargetPropertyName { get; private set; } + + private object sourceDataContext; + private PropertyInfo sourceBindingProperty; + private IValueConverter valueConverter; + + 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 (); } - } - } - - 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 interface IValueConverter { - object Convert (object value, object parameter = null); - } - - public class Binding { - public View Target { get; private set; } - public View Source { get; private set; } - - public string SourcePropertyName { get; private set; } - public string TargetPropertyName { get; private set; } - - private object sourceDataContext; - private PropertyInfo sourceBindingProperty; - private IValueConverter valueConverter; - - 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 void UpdateTarget () + { + try { + var sourceValue = sourceBindingProperty.GetValue (sourceDataContext); + if (sourceValue == null) { + return; } - } - private void UpdateTarget () - { - try { - var sourceValue = sourceBindingProperty.GetValue (sourceDataContext); - if (sourceValue == null) { - return; - } + var finalValue = valueConverter?.Convert (sourceValue) ?? sourceValue; - 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 UStringValueConverter : IValueConverter { - public object Convert (object value, object parameter = null) - { - var data = Encoding.ASCII.GetBytes (value.ToString ()); - return StringExtensions.ToString (data); + 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 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 45c6f704f..f2667caab 100644 --- a/UICatalog/Scenarios/Editor.cs +++ b/UICatalog/Scenarios/Editor.cs @@ -75,19 +75,19 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { _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 @@ namespace UICatalog.Scenarios { // 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { private void SetFindText () { - _textToFind = !string.IsNullOrEmpty(_textView.SelectedText) + _textToFind = !string.IsNullOrEmpty (_textView.SelectedText) ? _textView.SelectedText : string.IsNullOrEmpty (_textToFind) ? "" : _textToFind; @@ -809,7 +807,7 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 9d4ce4e32..3a162fa85 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]; - } - - 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); - } + Application.Init (); + ConfigurationManager.Themes.Theme = Theme; + ConfigurationManager.Apply (); + Application.Top.ColorScheme = Colors.ColorSchemes [TopLevelColorScheme]; } -} \ No newline at end of file + + 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); + } +} diff --git a/UICatalog/Scenarios/GraphViewExample.cs b/UICatalog/Scenarios/GraphViewExample.cs index 9f79be4c2..215fee170 100644 --- a/UICatalog/Scenarios/GraphViewExample.cs +++ b/UICatalog/Scenarios/GraphViewExample.cs @@ -85,7 +85,7 @@ namespace UICatalog.Scenarios { 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 c81853ca5..ead26d81b 100644 --- a/UICatalog/Scenarios/HexEditor.cs +++ b/UICatalog/Scenarios/HexEditor.cs @@ -55,10 +55,10 @@ namespace UICatalog.Scenarios { 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 26b26d1fd..5bf061a92 100644 --- a/UICatalog/Scenarios/InteractiveTree.cs +++ b/UICatalog/Scenarios/InteractiveTree.cs @@ -30,23 +30,23 @@ namespace UICatalog.Scenarios { 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 6594daa07..44efc1826 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 (); + + 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); + + edit.KeyDown += (s, a) => { + keyPressedList.Add (a.ToString ()); + }; + + + edit.InvokingKeyBindings += (s, a) => { + if (edit.KeyBindings.TryGet (a, out var binding)) { + invokingKeyBindingsList.Add ($"{a}: {string.Join (",", binding.Commands)}"); } + }; - public override bool ProcessHotKey (KeyEvent keyEvent) - { - _processHotKeyList.Add (keyEvent.ToString ()); - return base.ProcessHotKey (keyEvent); - } + // 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); - public override bool ProcessColdKey (KeyEvent keyEvent) - { - _processColdKeyList.Add (keyEvent.ToString ()); + edit.KeyDown += (s, e) => labelTextViewKeypress.Text = e.ToString (); - return base.ProcessColdKey (keyEvent); - } - } + 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); - public override void Init () + 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) { - 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 (); - } - - processColdKeyListView.ColorScheme = Colors.TopLevel; - Win.Add (processColdKeyListView); + // 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 6dd0fa1d8..a4fb8e0a4 100644 --- a/UICatalog/Scenarios/LineDrawing.cs +++ b/UICatalog/Scenarios/LineDrawing.cs @@ -33,7 +33,7 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { } } - 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 @@ namespace UICatalog.Scenarios { 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 c3c38f069..b71222cff 100644 --- a/UICatalog/Scenarios/ListColumns.cs +++ b/UICatalog/Scenarios/ListColumns.cs @@ -80,9 +80,9 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { Win.Add (selectedCellLabel); listColView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{listColView.SelectedRow},{listColView.SelectedColumn}"; }; - listColView.KeyPressed += TableViewKeyPress; + listColView.KeyDown += TableViewKeyPress; SetupScrollBar (); @@ -119,7 +119,7 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { } - 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 000000000..4677c1254 --- /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 c521d896a..56e13da56 100644 --- a/UICatalog/Scenarios/Notepad.cs +++ b/UICatalog/Scenarios/Notepad.cs @@ -6,7 +6,7 @@ using Terminal.Gui; 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 @@ namespace UICatalog.Scenarios { { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { //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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { }); } - ((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 @@ namespace UICatalog.Scenarios { 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); - if(tileIndex == -1) - { + var split = (TileView)sender.SuperView.SuperView; + var tileIndex = split.IndexOf (sender); + + 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 @@ namespace UICatalog.Scenarios { } if (result == 0) { - if(tab.File == null) { + if (tab.File == null) { SaveAs (); } else { tab.Save (); @@ -220,20 +219,20 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { if (string.IsNullOrWhiteSpace (fd.Path)) { return false; } - - if(fd.Canceled) { + + if (fd.Canceled) { return false; } @@ -338,8 +337,8 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { parent.SetNeedsDisplay (); } } else { - + if (Text.EndsWith ('*')) { Text = Text.TrimEnd ('*'); @@ -376,8 +375,8 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 dd9eef8dd..20c062ce0 100644 --- a/UICatalog/Scenarios/SendKeys.cs +++ b/UICatalog/Scenarios/SendKeys.cs @@ -57,17 +57,17 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 80b3693e6..2ab33ad5a 100644 --- a/UICatalog/Scenarios/SingleBackgroundWorker.cs +++ b/UICatalog/Scenarios/SingleBackgroundWorker.cs @@ -28,16 +28,16 @@ namespace UICatalog.Scenarios { { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 327480ba8..0c8f4cec9 100644 --- a/UICatalog/Scenarios/Snake.cs +++ b/UICatalog/Scenarios/Snake.cs @@ -124,21 +124,23 @@ namespace UICatalog.Scenarios { 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 bf477e8ea..5f9cf053f 100644 --- a/UICatalog/Scenarios/TableEditor.cs +++ b/UICatalog/Scenarios/TableEditor.cs @@ -100,9 +100,9 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { tableView.SelectedCellChanged += (s, e) => { selectedCellLabel.Text = $"{tableView.SelectedRow},{tableView.SelectedColumn}"; }; tableView.CellActivated += EditCurrentCell; - tableView.KeyPressed += TableViewKeyPress; + tableView.KeyDown += TableViewKeyPress; SetupScrollBar (); @@ -170,7 +170,7 @@ namespace UICatalog.Scenarios { } }; - tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); + tableView.KeyBindings.Add (KeyCode.Space, Command.ToggleChecked); } private void ShowAllColumns () @@ -386,13 +386,13 @@ namespace UICatalog.Scenarios { } - 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 6891b2ba2..48320ee4b 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -119,15 +119,15 @@ namespace UICatalog.Scenarios { } }; - 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 4efa3a967..50f5ca343 100644 --- a/UICatalog/Scenarios/TextViewAutocompletePopup.cs +++ b/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -87,8 +87,8 @@ namespace UICatalog.Scenarios { 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 7e4c72242..1c13c3841 100644 --- a/UICatalog/Scenarios/TreeViewFileSystem.cs +++ b/UICatalog/Scenarios/TreeViewFileSystem.cs @@ -95,7 +95,7 @@ namespace UICatalog.Scenarios { Win.Add (_detailsFrame); treeViewFiles.MouseClick += TreeViewFiles_MouseClick; - treeViewFiles.KeyPressed += TreeViewFiles_KeyPress; + treeViewFiles.KeyDown += TreeViewFiles_KeyPress; treeViewFiles.SelectionChanged += TreeViewFiles_SelectionChanged; SetupFileTree (); @@ -158,9 +158,9 @@ namespace UICatalog.Scenarios { } } - private void TreeViewFiles_KeyPress (object sender, KeyEventEventArgs obj) + private void TreeViewFiles_KeyPress (object sender, Key obj) { - if (obj.KeyEvent.Key == (Key.R | Key.CtrlMask)) { + if (obj.KeyCode == (KeyCode.R | KeyCode.CtrlMask)) { var selected = treeViewFiles.SelectedObject; diff --git a/UICatalog/Scenarios/Unicode.cs b/UICatalog/Scenarios/Unicode.cs index 0093303c3..e2d9eee84 100644 --- a/UICatalog/Scenarios/Unicode.cs +++ b/UICatalog/Scenarios/Unicode.cs @@ -30,8 +30,8 @@ namespace UICatalog.Scenarios { var statusBar = new StatusBar (new StatusItem [] { new StatusItem(Application.QuitKey, $"{Application.QuitKey} Выход", () => 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 f1d940f6e..4bb4fdd2e 100644 --- a/UICatalog/Scenarios/VkeyPacketSimulator.cs +++ b/UICatalog/Scenarios/VkeyPacketSimulator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; 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 @@ namespace UICatalog.Scenarios { Win.Add (inputVerticalRuler); var tvInput = new TextView { + Title = "Input", X = 1, Y = Pos.Bottom (inputHorizontalRuler), Width = Dim.Fill (), @@ -81,6 +83,7 @@ namespace UICatalog.Scenarios { Win.Add (outputVerticalRuler); var tvOutput = new TextView { + Title = "Output", X = 1, Y = Pos.Bottom (outputHorizontalRuler), Width = Dim.Fill (), @@ -88,24 +91,24 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { 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 @@ namespace UICatalog.Scenarios { } }; - 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 @@ namespace UICatalog.Scenarios { 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 3c0a3cb25..b8171714f 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 @@ using static Terminal.Gui.TableView; #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; +namespace UICatalog; - static readonly FileSystemWatcher _currentDirWatcher = new FileSystemWatcher (); - static readonly FileSystemWatcher _homeDirWatcher = new FileSystemWatcher (); +/// +/// 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 void Main (string [] args) - { - Console.OutputEncoding = Encoding.Default; + static readonly FileSystemWatcher _currentDirWatcher = new (); + static readonly FileSystemWatcher _homeDirWatcher = new (); - if (Debugger.IsAttached) { - CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); - } + static void Main (string [] args) + { + Console.OutputEncoding = Encoding.Default; - _scenarios = Scenario.GetScenarios (); - _categories = Scenario.GetAllCategories (); + if (Debugger.IsAttached) { + CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.GetCultureInfo ("en-US"); + } - if (args.Length > 0 && args.Contains ("-usc")) { - _useSystemConsole = true; - args = args.Where (val => val != "-usc").ToArray (); - } + _scenarios = Scenario.GetScenarios (); + _categories = Scenario.GetAllCategories (); - StartConfigFileWatcher (); + if (args.Length > 0 && args.Contains ("-usc")) { + _useSystemConsole = true; + args = args.Where (val => val != "-usc").ToArray (); + } - // If a Scenario name has been provided on the commandline - // run it and exit when done. - if (args.Length > 0) { - _topLevelColorScheme = "Base"; + StartConfigFileWatcher (); - 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; - } + // If a Scenario name has been provided on the commandline + // run it and exit when done. + if (args.Length > 0) { + _topLevelColorScheme = "Base"; - _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"); + 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; + } - 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 (); + _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"); - // This call to Application.Shutdown brackets the Application.Init call - // made by Scenario.Init() above - Application.Shutdown (); + while (RunUICatalogTopLevel () is { } scenario) { + VerifyObjectsWereDisposed (); + Themes!.Theme = _cachedTheme!; + Apply (); + scenario.Theme = _cachedTheme; + scenario.TopLevelColorScheme = _topLevelColorScheme; + scenario.Init (); + scenario.Setup (); + scenario.Run (); + scenario.Dispose (); - VerifyObjectsWereDisposed (); - } + // This call to Application.Shutdown brackets the Application.Init call + // made by Scenario.Init() above + Application.Shutdown (); - StopConfigFileWatcher (); VerifyObjectsWereDisposed (); } - private static void StopConfigFileWatcher () - { - _currentDirWatcher.EnableRaisingEvents = false; - _currentDirWatcher.Changed -= ConfigFileChanged; - _currentDirWatcher.Created -= ConfigFileChanged; + StopConfigFileWatcher (); + VerifyObjectsWereDisposed (); + } - _homeDirWatcher.EnableRaisingEvents = false; - _homeDirWatcher.Changed -= ConfigFileChanged; - _homeDirWatcher.Created -= ConfigFileChanged; + static void StopConfigFileWatcher () + { + _currentDirWatcher.EnableRaisingEvents = false; + _currentDirWatcher.Changed -= ConfigFileChanged; + _currentDirWatcher.Created -= ConfigFileChanged; + + _homeDirWatcher.EnableRaisingEvents = false; + _homeDirWatcher.Changed -= ConfigFileChanged; + _homeDirWatcher.Created -= ConfigFileChanged; + } + + 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"); + + if (!Directory.Exists (tuiDir)) { + Directory.CreateDirectory (tuiDir); + } + _currentDirWatcher.Path = tuiDir; + _currentDirWatcher.Filter = "*config.json"; + + // 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"); + + if (!Directory.Exists (tuiDir)) { + Directory.CreateDirectory (tuiDir); + } + _homeDirWatcher.Path = tuiDir; + _homeDirWatcher.Filter = "*config.json"; + + _currentDirWatcher.Changed += ConfigFileChanged; + //_currentDirWatcher.Created += ConfigFileChanged; + _currentDirWatcher.EnableRaisingEvents = true; + + _homeDirWatcher.Changed += ConfigFileChanged; + //_homeDirWatcher.Created += ConfigFileChanged; + _homeDirWatcher.EnableRaisingEvents = true; + } + + static void ConfigFileChanged (object sender, FileSystemEventArgs e) + { + if (Application.Top == null) { + return; } - 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"); + // TODO: This is a hack. Figure out how to ensure that the file is fully written before reading it. + //Thread.Sleep (500); + Load (); + Apply (); + } - if (!Directory.Exists (tuiDir)) { - Directory.CreateDirectory (tuiDir); - } - _currentDirWatcher.Path = tuiDir; - _currentDirWatcher.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; - // 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"); + // 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 (!Directory.Exists (tuiDir)) { - Directory.CreateDirectory (tuiDir); - } - _homeDirWatcher.Path = tuiDir; - _homeDirWatcher.Filter = "*config.json"; - - _currentDirWatcher.Changed += ConfigFileChanged; - //_currentDirWatcher.Created += ConfigFileChanged; - _currentDirWatcher.EnableRaisingEvents = true; - - _homeDirWatcher.Changed += ConfigFileChanged; - //_homeDirWatcher.Created += ConfigFileChanged; - _homeDirWatcher.EnableRaisingEvents = true; + if (_cachedTheme is null) { + _cachedTheme = Themes?.Theme; + } else { + Themes!.Theme = _cachedTheme; + Apply (); } - private static void ConfigFileChanged (object sender, FileSystemEventArgs e) + Application.Run (); + Application.Shutdown (); + + return _selectedScenario!; + } + + static List? _scenarios; + static List? _categories; + + // 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 StringBuilder? _aboutMessage = null; + + // If set, holds the scenario the user selected + static Scenario? _selectedScenario = null; + + static bool _useSystemConsole = false; + static ConsoleDriver.DiagnosticFlags _diagnosticFlags; + static bool _isFirstRunning = true; + static string _topLevelColorScheme = string.Empty; + + static MenuItem []? _themeMenuItems; + static MenuBarItem? _themeMenuBarItem; + + /// + /// 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; + CollectionNavigator _scenarioCollectionNav = new (); + + public StatusItem DriverName; + public StatusItem OS; + + public UICatalogTopLevel () { - if (Application.Top == null) { - return; - } + _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) + }) + }); - // 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 (); - } + DriverName = new StatusItem (Key.Empty, "Driver:", null); + OS = new StatusItem (Key.Empty, "OS:", null); - /// - /// 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; + StatusBar = new StatusBar () { + Visible = ShowStatusBar + }; - // 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 = CM.Themes?.Theme; - } else { - CM.Themes!.Theme = _cachedTheme; - CM.Apply (); - } - - Application.Run (); - Application.Shutdown (); - - return _selectedScenario!; - } - - static List? _scenarios; - static List? _categories; - - // 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 StringBuilder? _aboutMessage = null; - - // If set, holds the scenario the user selected - static Scenario? _selectedScenario = null; - - static bool _useSystemConsole = false; - static ConsoleDriver.DiagnosticFlags _diagnosticFlags; - static bool _isFirstRunning = true; - static string _topLevelColorScheme = string.Empty; - - static MenuItem []? _themeMenuItems; - static MenuBarItem? _themeMenuBarItem; - - /// - /// 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 - }; - - 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 - }; - - // 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 - */ - 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; - } + 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; - - // 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); - - // 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); - - KeyDown += KeyDownHandler; - - Add (CategoryList); - Add (ScenarioList); - - Add (MenuBar); - Add (StatusBar); - - Loaded += LoadedHandler; - Unloaded += UnloadedHandler; - - // Restore previous selections - CategoryList.SelectedItem = _cachedCategoryIndex; - ScenarioList.SelectedRow = _cachedScenarioIndex; - - CM.Applied += ConfigAppliedHandler; - } - - void LoadedHandler (object? sender, EventArgs? args) - { - ConfigChanged (); - - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; - DriverName.Title = $"Driver: {Driver.GetVersionInfo ()}"; - OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}"; - - if (_selectedScenario != null) { - _selectedScenario = null; - _isFirstRunning = false; - } - if (!_isFirstRunning) { - ScenarioList.SetFocus (); - } - - StatusBar.VisibleChanged += (s, e) => { - UICatalogApp.ShowStatusBar = StatusBar.Visible; - - var height = (StatusBar.Visible ? 1 : 0); - CategoryList.Height = Dim.Fill (height); - ScenarioList.Height = Dim.Fill (height); - // ContentPane.Height = Dim.Fill (height); + }), + new (Key.F10, "~F10~ Status Bar", () => { + StatusBar.Visible = !StatusBar.Visible; + //ContentPane!.Height = Dim.Fill(StatusBar.Visible ? 1 : 0); LayoutSubviews (); SetSubViewNeedsDisplay (); - }; + }), + DriverName, + OS + }; - Loaded -= LoadedHandler; - CategoryList.EnsureSelectedItemVisible (); - ScenarioList.EnsureSelectedCellIsVisible (); - } + // 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; - private void UnloadedHandler (object? sender, EventArgs? args) - { - CM.Applied -= ConfigAppliedHandler; - Unloaded -= UnloadedHandler; - } + // 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 + }; - void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) - { - ConfigChanged (); - } + // 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; - /// - /// 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; + /* 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 }); - // 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 ())!; - - // Tell the main app to stop - Application.RequestStop (); + // 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); + + Add (CategoryList); + Add (ScenarioList); + + Add (MenuBar); + Add (StatusBar); + + Loaded += LoadedHandler; + Unloaded += UnloadedHandler; + + // Restore previous selections + CategoryList.SelectedItem = _cachedCategoryIndex; + ScenarioList.SelectedRow = _cachedScenarioIndex; + + Applied += ConfigAppliedHandler; + } + + void LoadedHandler (object? sender, EventArgs? args) + { + ConfigChanged (); + + miIsMouseDisabled!.Checked = Application.IsMouseDisabled; + DriverName.Title = $"Driver: {Driver.GetVersionInfo ()}"; + OS.Title = $"OS: {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystem} {Microsoft.DotNet.PlatformAbstractions.RuntimeEnvironment.OperatingSystemVersion}"; + + if (_selectedScenario != null) { + _selectedScenario = null; + _isFirstRunning = false; + } + if (!_isFirstRunning) { + ScenarioList.SetFocus (); } - List CreateDiagnosticMenuItems () - { - List menuItems = new List { - CreateDiagnosticFlagsMenuItems (), - new MenuItem [] { null! }, - CreateDisabledEnabledMouseItems (), - CreateDisabledEnabledMenuBorder (), - CreateDisabledEnableUseSubMenusSingleFrame (), - CreateKeyBindingsMenuItems () - }; - return menuItems; + StatusBar.VisibleChanged += (s, e) => { + ShowStatusBar = StatusBar.Visible; + + int height = StatusBar.Visible ? 1 : 0; + CategoryList.Height = Dim.Fill (height); + ScenarioList.Height = Dim.Fill (height); + // ContentPane.Height = Dim.Fill (height); + LayoutSubviews (); + SetSubViewNeedsDisplay (); + }; + + Loaded -= LoadedHandler; + CategoryList.EnsureSelectedItemVisible (); + ScenarioList.EnsureSelectedCellIsVisible (); + } + + void UnloadedHandler (object? sender, EventArgs? args) + { + Applied -= ConfigAppliedHandler; + Unloaded -= UnloadedHandler; + } + + void ConfigAppliedHandler (object? sender, ConfigurationManagerEventArgs? a) + { + ConfigChanged (); + } + + /// + /// 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 ())!; + + // Tell the main app to stop + Application.RequestStop (); } + } - // 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); + List CreateDiagnosticMenuItems () + { + var menuItems = new List { + CreateDiagnosticFlagsMenuItems (), + new MenuItem [] { null! }, + CreateDisabledEnabledMouseItems (), + CreateDisabledEnabledMenuBorder (), + CreateDisabledEnableUseSubMenusSingleFrame (), + CreateKeyBindingsMenuItems () + }; + return menuItems; + } - return menuItems.ToArray (); - } + // 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); - // 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); + return menuItems.ToArray (); + } - return menuItems.ToArray (); - } + // 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 (); + } - MenuItem [] CreateForce16ColorItems () - { - List menuItems = new List (); - miForce16Colors = new MenuItem { - Title = "Force 16 _Colors", - Checked = Application.Force16Colors, - CanExecute = () => (bool)Application.Driver.SupportsTrueColor - }; - 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 (); - }; - menuItems.Add (miForce16Colors); + 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 (); - } + return menuItems.ToArray (); + } - MenuItem [] CreateDisabledEnabledMouseItems () - { - 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!; - }; - menuItems.Add (miIsMouseDisabled); + 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 () - { - List menuItems = new List (); + 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; + + var menuItems = new List (); + foreach (Enum diag in Enum.GetValues (_diagnosticFlags.GetType ())) { var item = new MenuItem { - Title = "_Key Bindings", - Help = "Change which keys do what" + Title = GetDiagnosticsTitle (diag), + Shortcut = (KeyCode)((Key)index.ToString () [0]).WithAlt }; + 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 += () => { - 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"; - 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; + 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 { - item.Checked = _diagnosticFlags.HasFlag (diag); - } - 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; + var f = GetDiagnosticsEnumValue (item.Title); + if (_diagnosticFlags.HasFlag (f)) { + SetDiagnosticsFlag (f, false); } else { - var f = GetDiagnosticsEnumValue (item.Title); - if (_diagnosticFlags.HasFlag (f)) { - SetDiagnosticsFlag (f, false); - } else { - SetDiagnosticsFlag (f, true); - } + SetDiagnosticsFlag (f, true); } - foreach (var menuItem in menuItems) { - if (menuItem.Title == t) { - menuItem.Checked = !_diagnosticFlags.HasFlag (ConsoleDriver.DiagnosticFlags.FrameRuler) + } + 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)); - } + } 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; } - } + 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, + _ => "" + }; } - public MenuItem []? CreateThemeMenuItems () + Enum GetDiagnosticsEnumValue (string title) { - 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); - } - - 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); - - return menuItems.ToArray (); + return title switch { + FRAME_RULER => ConsoleDriver.DiagnosticFlags.FrameRuler, + FRAME_PADDING => ConsoleDriver.DiagnosticFlags.FramePadding, + _ => null! + }; } - public void ConfigChanged () + void SetDiagnosticsFlag (Enum diag, bool add) { - miForce16Colors!.Checked = Application.Force16Colors; - - if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { - _topLevelColorScheme = "Base"; - } - - _themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems (); - _themeMenuBarItem!.Children = _themeMenuItems; - foreach (var mi in _themeMenuItems!) { - if (mi is { Parent: null }) { - mi.Parent = _themeMenuBarItem; + 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; } - - 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; - } - - var schemeMenuItems = ((MenuBarItem)_themeMenuItems?.Where (i => i is MenuBarItem)!.FirstOrDefault ()!)!.Children; - foreach (var schemeMenuItem in schemeMenuItems) { - schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; - } - - ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; - - //ContentPane.LineStyle = FrameView.DefaultBorderStyle; - - MenuBar.Menus [0].Children [0].Shortcut = Application.QuitKey; - StatusBar.Items [0].Shortcut = Application.QuitKey; - StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit"; - - miIsMouseDisabled!.Checked = Application.IsMouseDisabled; - - var height = (UICatalogApp.ShowStatusBar ? 1 : 0);// + (MenuBar.Visible ? 1 : 0); - //ContentPane.Height = Dim.Fill (height); - - StatusBar.Visible = UICatalogApp.ShowStatusBar; - - Application.Top.SetNeedsDisplay (); - } - - void KeyDownHandler (object? sender, KeyEventEventArgs? a) - { - if (a!.KeyEvent.IsCapslock) { - Capslock.Title = "Caps: On"; - StatusBar.SetNeedsDisplay (); - } else { - Capslock.Title = "Caps: Off"; - StatusBar.SetNeedsDisplay (); - } - - if (a!.KeyEvent.IsNumlock) { - Numlock.Title = "Num: On"; - StatusBar.SetNeedsDisplay (); - } else { - Numlock.Title = "Num: Off"; - StatusBar.SetNeedsDisplay (); - } - - if (a!.KeyEvent.IsScrolllock) { - Scrolllock.Title = "Scroll: On"; - StatusBar.SetNeedsDisplay (); - } else { - Scrolllock.Title = "Scroll: Off"; - StatusBar.SetNeedsDisplay (); - } - } - - 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; - } } - static void VerifyObjectsWereDisposed () + public MenuItem []? CreateThemeMenuItems () { -#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) { + var menuItems = CreateForce16ColorItems ().ToList (); + menuItems.Add (null!); - Debug.Assert (inst.WasDisposed); + 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); } - 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); + 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); } - RunState.Instances.Clear (); -#endif + menuItems.Add (null!); + var mbi = new MenuBarItem ("_Color Scheme for Application.Top", schemeMenuItems.ToArray ()); + menuItems.Add (mbi); + + return menuItems.ToArray (); } - static void OpenUrl (string url) + public void ConfigChanged () { - 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; + miForce16Colors!.Checked = Application.Force16Colors; + + if (_topLevelColorScheme == null || !Colors.ColorSchemes.ContainsKey (_topLevelColorScheme)) { + _topLevelColorScheme = "Base"; } + + _themeMenuItems = ((UICatalogTopLevel)Application.Top).CreateThemeMenuItems (); + _themeMenuBarItem!.Children = _themeMenuItems; + foreach (var mi in _themeMenuItems!) { + if (mi is { Parent: null }) { + mi.Parent = _themeMenuBarItem; + } + } + + 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; + } + + var schemeMenuItems = ((MenuBarItem)_themeMenuItems?.Where (i => i is MenuBarItem)!.FirstOrDefault ()!)!.Children; + foreach (var schemeMenuItem in schemeMenuItems) { + schemeMenuItem.Checked = (string)schemeMenuItem.Data == _topLevelColorScheme; + } + + ColorScheme = Colors.ColorSchemes [_topLevelColorScheme]; + + //ContentPane.LineStyle = FrameView.DefaultBorderStyle; + + MenuBar.Menus [0].Children [0].Shortcut = (KeyCode)Application.QuitKey; + StatusBar.Items [0].Shortcut = Application.QuitKey; + StatusBar.Items [0].Title = $"~{Application.QuitKey} to quit"; + + miIsMouseDisabled!.Checked = Application.IsMouseDisabled; + + int height = ShowStatusBar ? 1 : 0; // + (MenuBar.Visible ? 1 : 0); + //ContentPane.Height = Dim.Fill (height); + + StatusBar.Visible = ShowStatusBar; + + Application.Top.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!; + + } 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 (int i = 0; i < ScenarioList.Table.Rows; i++) { + firstColumnList.Add (ScenarioList.Table [i, 0]); + } + _scenarioCollectionNav.Collection = firstColumnList; + } } -} + + 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) { + + 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 + } + + 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 5e2393d2e..d0a9e63bb 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 2a54dadcf..3c101a0bb 100644 --- a/UnitTests/Application/ApplicationTests.cs +++ b/UnitTests/Application/ApplicationTests.cs @@ -13,11 +13,11 @@ using Console = Terminal.Gui.FakeConsole; 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 class ApplicationTests { │ │ │ │ │ │ - └───┘", output); + └───┘", _output); var attributes = new Attribute [] { // 0 @@ -498,7 +498,7 @@ public class ApplicationTests { │ │ │ │ │ │ - └───┘", output); + └───┘", _output); attributes = new Attribute [] { // 0 @@ -654,316 +654,11 @@ public class ApplicationTests { 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 class ApplicationTests { 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 000000000..97facc89c --- /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 e8bf7b91c..d8bd2d1a2 100644 --- a/UnitTests/Application/MainLoopTests.cs +++ b/UnitTests/Application/MainLoopTests.cs @@ -627,8 +627,8 @@ public class MainLoopTests { 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 class MainLoopTests { 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 class MainLoopTests { } 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 1440780f9..9c1f33d69 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; +#if RUN_CLIPBOARD_UNIT_TESTS +public class ClipboardTests { + readonly ITestOutputHelper output; + + public ClipboardTests (ITestOutputHelper output) + { + this.output = output; + } + + [Fact] [AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] + public void IClipboard_GetClipBoardData_Throws_NotSupportedException () + { + var iclip = Application.Driver.Clipboard; + Assert.Throws (() => iclip.GetClipboardData ()); + } + + [Fact] [AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] + public void IClipboard_SetClipBoardData_Throws_NotSupportedException () + { + var 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; } - [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] - public void IClipboard_GetClipBoardData_Throws_NotSupportedException () - { - IClipboard iclip = Application.Driver.Clipboard; - Assert.Throws (() => iclip.GetClipboardData ()); + 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 Contents_Gets_Sets () + { + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: true, fakeClipboardAlwaysThrowsNotSupportedException: true)] - public void IClipboard_SetClipBoardData_Throws_NotSupportedException () - { - IClipboard iclip = Application.Driver.Clipboard; - Assert.Throws (() => iclip.SetClipboardData ("foo")); + 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 Contents_Gets_Sets_When_IsSupportedFalse () + { + + if (!Clipboard.IsSupported) { + output.WriteLine ($"The Clipboard not supported on this platform."); + return; } - [Fact, AutoInitShutdown (useFakeClipboard: true)] - public void Contents_Fake_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); + } + [Fact] [AutoInitShutdown (useFakeClipboard: true)] + public void Contents_Fake_Gets_Sets_When_IsSupportedFalse () + { + + 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; + + 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 () + { + 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.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); + } + } + + [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.Run (); + + if (Clipboard.IsSupported) { Assert.Equal (clipText, Clipboard.Contents); + } else { + Assert.NotEqual (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; - } - - 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 (); - - Assert.Equal (clipText, Clipboard.Contents); - } - - [Fact, AutoInitShutdown (useFakeClipboard: false)] - public void Contents_Gets_Sets_When_IsSupportedFalse () - { - - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } - - 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 (); - - Assert.Equal (clipText, Clipboard.Contents); - } - - [Fact, AutoInitShutdown (useFakeClipboard: true)] - public void Contents_Fake_Gets_Sets_When_IsSupportedFalse () - { - - if (!Clipboard.IsSupported) { - output.WriteLine ($"The Clipboard not supported on this platform."); - return; - } - - 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 (); - - 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; - - Application.Iteration += (s, a) => Application.RequestStop (); - - 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); - } - } - - [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)); - - Application.Iteration += (s, a) => Application.RequestStop (); - - Application.Run (); - - 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 @@ namespace Terminal.Gui.ClipboardTests { } #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 5bd36a38c..70b2adadb 100644 --- a/UnitTests/Configuration/ConfigurationMangerTests.cs +++ b/UnitTests/Configuration/ConfigurationMangerTests.cs @@ -217,16 +217,16 @@ namespace Terminal.Gui.ConfigurationTests { 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 @@ namespace Terminal.Gui.ConfigurationTests { // 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 @@ namespace Terminal.Gui.ConfigurationTests { // 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 @@ namespace Terminal.Gui.ConfigurationTests { 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 @@ namespace Terminal.Gui.ConfigurationTests { { ""$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 @@ namespace Terminal.Gui.ConfigurationTests { 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 @@ namespace Terminal.Gui.ConfigurationTests { // 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 @@ namespace Terminal.Gui.ConfigurationTests { { 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 @@ namespace Terminal.Gui.ConfigurationTests { { 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 @@ namespace Terminal.Gui.ConfigurationTests { { 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 ed5ff6d6d..41a5650bb 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 { +namespace Terminal.Gui.ConfigurationTests; - [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}\""; +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); + // Act + var 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); - } + // 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); + [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 } }; - // 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); - } + string serialized = JsonSerializer.Serialize (new Color (colorName), options); - [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); - } + 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); + [Fact] + public void TestSerializeColor_Black () + { + // Arrange + string expectedJson = "\"Black\""; - // Act - var actualColorScheme = JsonSerializer.Deserialize (serializedColorScheme, ConfigurationManagerTests._jsonOptions); + // Act + string json = JsonSerializer.Serialize (new Color (Color.Black), new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); - // Assert - Assert.Equal (expectedColorScheme, actualColorScheme); - } + // Assert + Assert.Equal (expectedJson, 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 ()); + [Fact] + public void TestSerializeColor_BrightRed () + { + // Arrange + string expectedJson = "\"BrightRed\""; - // Act - var json = JsonSerializer.Serialize (key, options); - var deserializedKey = JsonSerializer.Deserialize (json, options); + // Act + string json = JsonSerializer.Serialize (new Color (Color.BrightRed), new JsonSerializerOptions { + Converters = { new ColorJsonConverter () } + }); - // Assert - Assert.Equal (expectedStringTo, deserializedKey.ToString ()); - } + // 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); + } + + [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 b7d89f258..571f302c6 100644 --- a/UnitTests/Configuration/SettingsScopeTests.cs +++ b/UnitTests/Configuration/SettingsScopeTests.cs @@ -38,42 +38,42 @@ namespace Terminal.Gui.ConfigurationTests { 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 e54a6e1b5..d3c09b80e 100644 --- a/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs +++ b/UnitTests/ConsoleDrivers/ConsoleDriverTests.cs @@ -57,11 +57,13 @@ namespace Terminal.Gui.DriverTests { 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 @@ namespace Terminal.Gui.DriverTests { 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 @@ namespace Terminal.Gui.DriverTests { // 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 @@ namespace Terminal.Gui.DriverTests { // 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 000000000..26cb60ec5 --- /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 000000000..ef1801ecd --- /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 b7ad45b28..000000000 --- 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 e01885ef9..e39686e2b 100644 --- a/UnitTests/Dialogs/DialogTests.cs +++ b/UnitTests/Dialogs/DialogTests.cs @@ -799,7 +799,7 @@ namespace Terminal.Gui.DialogTests { 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 @@ namespace Terminal.Gui.DialogTests { └──────────────────────────────────────────────────────────────────┘"; 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 @@ namespace Terminal.Gui.DialogTests { │ {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 4eb66725d..666ca24fc 100644 --- a/UnitTests/Drawing/ThicknessTests.cs +++ b/UnitTests/Drawing/ThicknessTests.cs @@ -615,14 +615,12 @@ public class ThicknessTests { [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 class ThicknessTests { [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 460dbef88..14b89a281 100644 --- a/UnitTests/FileServices/FileDialogTests.cs +++ b/UnitTests/FileServices/FileDialogTests.cs @@ -104,8 +104,13 @@ namespace Terminal.Gui.FileServicesTests { 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 @@ namespace Terminal.Gui.FileServicesTests { // 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 @@ namespace Terminal.Gui.FileServicesTests { 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 @@ namespace Terminal.Gui.FileServicesTests { 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 @@ namespace Terminal.Gui.FileServicesTests { │ │ │ │ │ │ -│{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 @@ namespace Terminal.Gui.FileServicesTests { ││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 000000000..0dde97087 --- /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 000000000..8e2f3531b --- /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 304d771c8..005137d11 100644 --- a/UnitTests/Input/ResponderTests.cs +++ b/UnitTests/Input/ResponderTests.cs @@ -3,118 +3,130 @@ using Xunit; // 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; - [Fact, TestRespondersDisposed] - public void New_Methods_Return_False () - { - var r = new Responder (); +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 (); + } - 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 (); + [Fact] [TestRespondersDisposed] + public void New_Methods_Return_False () + { + var r = new View (); - r.Dispose (); - } + //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 })); - // Generic lifetime (IDisposable) tests - [Fact, TestRespondersDisposed] - public void Dispose_Works () - { - - var r = new Responder (); + 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; - } - } - - [Fact, TestRespondersDisposed] - public void IsOverridden_False_IfNotOverridden () + public override bool OnKeyDown (Key keyEvent) { - // 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 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")); - -#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); -#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")); - - // 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")); - - // 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")); -#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); -#endif + return true; } } -} + + [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 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")); + +#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); +#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")); + + //// 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")); + + // 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")); +#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); +#endif + } +} \ No newline at end of file diff --git a/UnitTests/TestHelpers.cs b/UnitTests/TestHelpers.cs index 6f09942a5..b6a34c581 100644 --- a/UnitTests/TestHelpers.cs +++ b/UnitTests/TestHelpers.cs @@ -12,6 +12,7 @@ using Attribute = Terminal.Gui.Attribute; 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 @@ partial class TestHelpers { 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 dcf909fc8..5f853abba 100644 --- a/UnitTests/Text/AutocompleteTests.cs +++ b/UnitTests/Text/AutocompleteTests.cs @@ -92,7 +92,7 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 9031b157a..f3f2271bb 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 @@ namespace Terminal.Gui.TextTests { "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 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 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 @@ namespace Terminal.Gui.TextTests { "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 @@ namespace Terminal.Gui.TextTests { "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 @@ namespace Terminal.Gui.TextTests { "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 @@ namespace Terminal.Gui.TextTests { "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 @@ namespace Terminal.Gui.TextTests { "$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 @@ namespace Terminal.Gui.TextTests { "$200.00", "appricot" }; - int current = 0; - var n = new CollectionNavigator (strings); + 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); + // 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); + // 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 ("$$"), 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 ("$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.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, '$')); + 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); - } + 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,144 +272,133 @@ namespace Terminal.Gui.TextTests { "yellow", "zebra" }; - int current = 0; - var n = new CollectionNavigator (strings); + 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. + // 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); + 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 [] { - "$$", - "$100.00", - "$101.00", - "$101.10", - "$200.00", - "appricot", - "c", - "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)); - } - - [Fact] - public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () - { - var strings = new string [] { - "$$", - "$100.00", - "$101.00", - "$101.10", - "$200.00", - "appricot", - "c", - "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)); - } - - [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)); - } - } - - // 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, - }))); - } + // 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 [] { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot", + "c", + "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)); + } + + [Fact] + public void MinimizeMovement_True_ShouldStayOnCurrentIfMultipleMatches () + { + var strings = new string [] { + "$$", + "$100.00", + "$101.00", + "$101.10", + "$200.00", + "appricot", + "c", + "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)); + } + + [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 (KeyCode.Null))); + } +} \ No newline at end of file diff --git a/UnitTests/Text/TextFormatterTests.cs b/UnitTests/Text/TextFormatterTests.cs index 99756f0f1..b6b7969bb 100644 --- a/UnitTests/Text/TextFormatterTests.cs +++ b/UnitTests/Text/TextFormatterTests.cs @@ -227,33 +227,33 @@ namespace Terminal.Gui.TextTests { 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); + Assert.Equal (KeyCode.Null, 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 (К) + [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 @@ namespace Terminal.Gui.TextTests { } [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 @@ namespace Terminal.Gui.TextTests { } [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 @@ namespace Terminal.Gui.TextTests { } [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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 @@ namespace Terminal.Gui.TextTests { 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 f95107353..6a971ab3c 100644 --- a/UnitTests/UICatalog/ScenarioTests.cs +++ b/UnitTests/UICatalog/ScenarioTests.cs @@ -29,13 +29,13 @@ namespace UICatalog.Tests { { 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 @@ namespace UICatalog.Tests { // 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 @@ namespace UICatalog.Tests { // 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 @@ namespace UICatalog.Tests { } }; - 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 06c25a881..0da497ab8 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 000000000..21e8775f7 --- /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 000000000..fd8edf035 --- /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 a9557294b..000000000 --- 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 c70b66d45..356934744 100644 --- a/UnitTests/View/Layout/DimTests.cs +++ b/UnitTests/View/Layout/DimTests.cs @@ -688,7 +688,7 @@ namespace Terminal.Gui.ViewTests { 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 @@ namespace Terminal.Gui.ViewTests { }; 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 @@ namespace Terminal.Gui.ViewTests { var listLabels = new List