From ea7981dc5987ed8a3872118bfb40a2698b89850e Mon Sep 17 00:00:00 2001 From: Thomas Nind <31306100+tznind@users.noreply.github.com> Date: Tue, 8 Feb 2022 18:40:40 +0000 Subject: [PATCH] Adds Key Binding support. Also refactors Autocomplete and Undo/Redo. (#1556) * Refactored ProcessKey to use public methods for case logic * Added KeyBinding class * Refactored key binding to split key->command from command->implementation This reduces duplication and simplifies the API * Finishing key bindings implementation in ListView. * Adding more unit tests to the ListView. * Added key bindings to the Button and more features. * Replaces Action for Func on CommandImplementations. * Allowing commands to have any number of arguments. * Implementing key bindings on Checkbox view. * Added test for changing HotKey in Button and made ReplaceKeyBinding protected * Changed `CommandImplementations` to `Func` to better understand current command implementations * Implementing key bindings in ComboBox. * Renamed Command keys and fixed ComboBox issues: - Fixed pressing Esc in ListAndCombos scenario without selecting cause an array out of bounds error - Changed the Esc key in ComboBox to also collapse the list selection - Added bool return to public virtual method Expand and Collapse (this is a breaking change) * Implementing key bindings in DateField. * Organizing some things. * Implementing key bindings on TimeField. * No key bindings on FrameView. * Added keybinding support to TreeView * Added mouse support and more features. * Updating NuGet packages. * Putting text on the same line. * Changing function command to Func. * Added a read only Position, CursorPosition properties and events. * Keybindings for GraphView * Added a stream argument to ApplyEdits to only save the edits. * Implementing key bindings on the HexView. * Added MenuOpened event and others bug fixes. * Fixing typo. * Unifying constructors initializations. * Implementing keybindings in the Menu. * Removing unnecessary variable. * Implementing keybindings in RadioGroup view. * Changing Home to TopHome and End to BottomEnd. * Implementing keybindings in the ScrollView. * Changing the PageLeft and PageRight keybindings. * Fixing PageLeft and RightPage. * Removing CleanUp command. * Key bindings for TabView * Keybindings for TableView * Fixed unit tests for PageDown to correctly assign input focus to the TableView * Fixes the CalculateLeftColumn method avoiding jump two columns on forward moving. * Fixes #1525. Gives the same backspace behavior as TextView. * Changes kill-to-start key to work on Linux too. * Fixes SelectedStart, SelectedText and some cleaning. * Implementing keybindings in TextField. * Updated command names and merged as discussed with @BDisp - Merged LeftItem and LeftChar to Left (same for Right). - Also renamed Kill to Cut - Added ScrollLeft / ScrollRight (and renamed ScrollLineUp to just ScrollUp * Renamed Command.InsertChar to ToggleOverwriteMode and added Enable/Disable * Removed 'Mode' suffix from toggle overwrite * Allows navigation to outside a TextView if IsMdiContainer is true. * Implementing keybindings in Toplevel. * Fixing null reference exception. * Changing to keys instances events instead static. * Transferring the events to the Toplevel. * Implementing keybindings in TextView. * Removing static from the QuitKeyChanged and adding unit test. * Replacing Added with the Initialized event. * Ignore control characters and other special keys. * Changing InvokeKeybindings to return Nullable bool and added two more keys to the Toplevel. * Implementing keybindings in Autocomplete. I had to derive from View. * Added keybindings menu item to UICatalog * Added ClearBinding * Implementing IAutocomplete, abstract Autocomplete and derived TextViewAutocomplete. * Implementing keybindings in the TextValidateProvider * Add keybinding to CellActivationKey. * Fixing some formats. * Add ObjectActivationKey to the keybindings. * Made it much easier to implement abstract base `Autocomplete` in other views by moving methods up out of `TextViewAutocomplete` implementation * Allowing Autocomplete to popup inside or outside the container. * Fixes the cursor not being showing if the text length is equal to the view width. * A unit test to prove the 4df5897. * Removed unused method `GetCursorPosition` from Autocomplete * Trimmed down implementation specific methods from IAutocomplete * Fixed xmldoc comment tag * Format Autocomplete on multiline and fixes wrap settings. * Adding keys from a to z to avoid the Key.Space on ToString. * Fixes the vertical position outside the container. * Adding more key unit tests. * Changing comment to upper case and proving that doesn't will breaking nothing. * Replaces Pos.Bottom to Pos.AnchorEnd. * Fixes popup on resizing. * Should only using the Pos.Bottom to position outside the view. * Fixes #1584 * Fixes https://github.com/migueldeicaza/gui.cs/issues/1584#issuecomment-1027987475 * Fixes some bugs with SelectedItem. * Command must also return a nullable bool. * Ensures updating the ComboBox text on leaving the control. * Only with the nullable bool was possible to make the MoveUp and the MoveDown working. * Added logging of which scenario failed in test Co-authored-by: BDisp --- Terminal.Gui/Core/Application.cs | 62 +- Terminal.Gui/Core/Autocomplete.cs | 305 ---- .../Core/Autocomplete/Autocomplete.cs | 642 ++++++++ .../Core/Autocomplete/IAutocomplete.cs | 114 ++ Terminal.Gui/Core/Command.cs | 388 +++++ Terminal.Gui/Core/Event.cs | 105 +- Terminal.Gui/Core/TextFormatter.cs | 19 +- Terminal.Gui/Core/Toplevel.cs | 222 ++- Terminal.Gui/Core/View.cs | 189 ++- Terminal.Gui/Views/Button.cs | 93 +- Terminal.Gui/Views/Checkbox.cs | 56 +- Terminal.Gui/Views/ComboBox.cs | 218 ++- Terminal.Gui/Views/DateField.cs | 175 +- Terminal.Gui/Views/GraphView.cs | 66 +- Terminal.Gui/Views/HexView.cs | 144 +- Terminal.Gui/Views/ListView.cs | 83 +- Terminal.Gui/Views/Menu.cs | 128 +- Terminal.Gui/Views/RadioGroup.cs | 160 +- Terminal.Gui/Views/ScrollView.cs | 70 +- Terminal.Gui/Views/TabView.cs | 35 +- Terminal.Gui/Views/TableView.cs | 230 ++- Terminal.Gui/Views/TextField.cs | 412 +++-- Terminal.Gui/Views/TextValidateField.cs | 54 +- Terminal.Gui/Views/TextView.cs | 1402 +++++++++++------ Terminal.Gui/Views/TimeField.cs | 171 +- Terminal.Gui/Views/TreeView.cs | 282 ++-- UICatalog/KeyBindingsDialog.cs | 199 +++ .../Scenarios/BackgroundWorkerCollection.cs | 2 +- UICatalog/Scenarios/BordersComparisons.cs | 36 +- UICatalog/Scenarios/Text.cs | 21 +- .../Scenarios/TextViewAutocompletePopup.cs | 186 +++ UICatalog/UICatalog.cs | 17 + UnitTests/ApplicationTests.cs | 9 + UnitTests/AutocompleteTests.cs | 118 +- UnitTests/ButtonTests.cs | 103 ++ UnitTests/CheckboxTests.cs | 63 + UnitTests/ComboBoxTests.cs | 194 ++- UnitTests/DateFieldTests.cs | 98 ++ UnitTests/FrameViewTests.cs | 36 + UnitTests/HexViewTests.cs | 58 + UnitTests/KeyTests.cs | 35 +- UnitTests/ListViewTests.cs | 88 ++ UnitTests/MenuTests.cs | 5 +- UnitTests/PosTests.cs | 128 ++ UnitTests/RadioGroupTests.cs | 116 ++ UnitTests/ScenarioTests.cs | 10 +- UnitTests/ScrollViewTests.cs | 175 ++ UnitTests/TableViewTests.cs | 15 +- UnitTests/TextFieldTests.cs | 4 + UnitTests/TextViewTests.cs | 474 +++++- UnitTests/TimeFieldTests.cs | 98 ++ UnitTests/ToplevelTests.cs | 333 +++- UnitTests/ViewTests.cs | 25 + 53 files changed, 6582 insertions(+), 1889 deletions(-) delete mode 100644 Terminal.Gui/Core/Autocomplete.cs create mode 100644 Terminal.Gui/Core/Autocomplete/Autocomplete.cs create mode 100644 Terminal.Gui/Core/Autocomplete/IAutocomplete.cs create mode 100644 Terminal.Gui/Core/Command.cs create mode 100644 UICatalog/KeyBindingsDialog.cs create mode 100644 UICatalog/Scenarios/TextViewAutocompletePopup.cs create mode 100644 UnitTests/ButtonTests.cs create mode 100644 UnitTests/CheckboxTests.cs create mode 100644 UnitTests/DateFieldTests.cs create mode 100644 UnitTests/FrameViewTests.cs create mode 100644 UnitTests/ListViewTests.cs create mode 100644 UnitTests/RadioGroupTests.cs create mode 100644 UnitTests/ScrollViewTests.cs create mode 100644 UnitTests/TimeFieldTests.cs diff --git a/Terminal.Gui/Core/Application.cs b/Terminal.Gui/Core/Application.cs index 2d27d66a3..57bcf9fdc 100644 --- a/Terminal.Gui/Core/Application.cs +++ b/Terminal.Gui/Core/Application.cs @@ -140,18 +140,74 @@ namespace Terminal.Gui { } } + static Key alternateForwardKey = Key.PageDown | Key.CtrlMask; + /// /// Alternative key to navigate forwards through all views. Ctrl+Tab is always used. /// - public static Key AlternateForwardKey { get; set; } = Key.PageDown | Key.CtrlMask; + public static Key AlternateForwardKey { + get => alternateForwardKey; + set { + if (alternateForwardKey != value) { + var oldKey = alternateForwardKey; + alternateForwardKey = value; + OnAlternateForwardKeyChanged (oldKey); + } + } + } + + static void OnAlternateForwardKeyChanged (Key oldKey) + { + foreach (var top in toplevels) { + top.OnAlternateForwardKeyChanged (oldKey); + } + } + + static Key alternateBackwardKey = Key.PageUp | Key.CtrlMask; + /// /// Alternative key to navigate backwards through all views. Shift+Ctrl+Tab is always used. /// - public static Key AlternateBackwardKey { get; set; } = Key.PageUp | Key.CtrlMask; + public static Key AlternateBackwardKey { + get => alternateBackwardKey; + set { + if (alternateBackwardKey != value) { + var oldKey = alternateBackwardKey; + alternateBackwardKey = value; + OnAlternateBackwardKeyChanged (oldKey); + } + } + } + + static void OnAlternateBackwardKeyChanged (Key oldKey) + { + foreach (var top in toplevels) { + top.OnAlternateBackwardKeyChanged (oldKey); + } + } + + static Key quitKey = Key.Q | Key.CtrlMask; + /// /// Gets or sets the key to quit the application. /// - public static Key QuitKey { get; set; } = Key.Q | Key.CtrlMask; + public static Key QuitKey { + get => quitKey; + set { + if (quitKey != value) { + var oldKey = quitKey; + quitKey = value; + OnQuitKeyChanged (oldKey); + } + } + } + + static void OnQuitKeyChanged (Key oldKey) + { + foreach (var top in toplevels) { + top.OnQuitKeyChanged (oldKey); + } + } /// /// The driver for the application diff --git a/Terminal.Gui/Core/Autocomplete.cs b/Terminal.Gui/Core/Autocomplete.cs deleted file mode 100644 index 3b9660649..000000000 --- a/Terminal.Gui/Core/Autocomplete.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; -using System.Text; -using Rune = System.Rune; - -namespace Terminal.Gui { - - /// - /// Renders an overlay on another view at a given point that allows selecting - /// from a range of 'autocomplete' options. - /// - public class Autocomplete { - - /// - /// The maximum width of the autocomplete dropdown - /// - public int MaxWidth { get; set; } = 10; - - /// - /// The maximum number of visible rows in the autocomplete dropdown to render - /// - public int MaxHeight { get; set; } = 6; - - /// - /// True if the autocomplete should be considered open and visible - /// - protected bool Visible { get; set; } = true; - - /// - /// The strings that form the current list of suggestions to render - /// based on what the user has typed so far. - /// - public ReadOnlyCollection Suggestions { get; protected set; } = new ReadOnlyCollection(new string[0]); - - /// - /// The full set of all strings that can be suggested. - /// - /// - public List AllSuggestions { get; set; } = new List(); - - /// - /// The currently selected index into that the user has highlighted - /// - public int SelectedIdx { get; set; } - - /// - /// When more suggestions are available than can be rendered the user - /// can scroll down the dropdown list. This indicates how far down they - /// have gone - /// - public int ScrollOffset {get;set;} - - /// - /// The colors to use to render the overlay. Accessing this property before - /// the Application has been initialised will cause an error - /// - public ColorScheme ColorScheme { - get - { - if(colorScheme == null) - { - colorScheme = Colors.Menu; - } - return colorScheme; - } - set - { - colorScheme = value; - } - } - - private ColorScheme colorScheme; - - /// - /// The key that the user must press to accept the currently selected autocomplete suggestion - /// - public Key SelectionKey { get; set; } = Key.Enter; - - /// - /// The key that the user can press to close the currently popped autocomplete menu - /// - public Key CloseKey {get;set;} = Key.Esc; - - /// - /// Renders the autocomplete dialog inside the given at the - /// given point. - /// - /// The view the overlay should be rendered into - /// - public void RenderOverlay (View view, Point renderAt) - { - if (!Visible || !view.HasFocus || Suggestions.Count == 0) { - return; - } - - view.Move (renderAt.X, renderAt.Y); - - // don't overspill vertically - var height = Math.Min(view.Bounds.Height - renderAt.Y,MaxHeight); - - var toRender = Suggestions.Skip(ScrollOffset).Take(height).ToArray(); - - if(toRender.Length == 0) - { - return; - } - - var width = Math.Min(MaxWidth,toRender.Max(s=>s.Length)); - - // don't overspill horizontally - width = Math.Min(view.Bounds.Width - renderAt.X ,width); - - for(int i=0;i - /// Updates to be a valid index within - /// - public void EnsureSelectedIdxIsValid() - { - SelectedIdx = Math.Max (0,Math.Min (Suggestions.Count - 1, SelectedIdx)); - - // if user moved selection up off top of current scroll window - if(SelectedIdx < ScrollOffset) - { - ScrollOffset = SelectedIdx; - } - - // if user moved selection down past bottom of current scroll window - while(SelectedIdx >= ScrollOffset + MaxHeight ){ - ScrollOffset++; - } - } - - /// - /// Handle key events before e.g. to make key events like - /// up/down apply to the autocomplete control instead of changing the cursor position in - /// the underlying text view. - /// - /// - /// - /// - public bool ProcessKey (TextView hostControl, KeyEvent kb) - { - if(IsWordChar((char)kb.Key)) - { - Visible = true; - } - - if(!Visible || Suggestions.Count == 0) { - return false; - } - - if (kb.Key == Key.CursorDown) { - SelectedIdx++; - EnsureSelectedIdxIsValid(); - hostControl.SetNeedsDisplay (); - return true; - } - - if (kb.Key == Key.CursorUp) { - SelectedIdx--; - EnsureSelectedIdxIsValid(); - hostControl.SetNeedsDisplay (); - return true; - } - - if(kb.Key == SelectionKey && SelectedIdx >=0 && SelectedIdx < Suggestions.Count) { - - var accepted = Suggestions [SelectedIdx]; - - var typedSoFar = GetCurrentWord (hostControl) ?? ""; - - if(typedSoFar.Length < accepted.Length) { - - // delete the text - for(int i=0;i - /// Clears - /// - public void ClearSuggestions () - { - Suggestions = Enumerable.Empty ().ToList ().AsReadOnly (); - } - - - /// - /// Populates with all strings in that - /// match with the current cursor position/text in the - /// - /// The text view that you want suggestions for - public void GenerateSuggestions (TextView hostControl) - { - // if there is nothing to pick from - if(AllSuggestions.Count == 0) { - ClearSuggestions (); - return; - } - - var currentWord = GetCurrentWord (hostControl); - - if(string.IsNullOrWhiteSpace(currentWord)) { - ClearSuggestions (); - } - else { - Suggestions = AllSuggestions.Where (o => - o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) && - !o.Equals(currentWord,StringComparison.CurrentCultureIgnoreCase) - ).ToList ().AsReadOnly(); - - EnsureSelectedIdxIsValid(); - } - } - - private string GetCurrentWord (TextView hostControl) - { - var currentLine = hostControl.GetCurrentLine (); - var cursorPosition = Math.Min (hostControl.CurrentColumn, currentLine.Count); - return IdxToWord (currentLine, cursorPosition); - } - - private string IdxToWord (List line, int idx) - { - StringBuilder sb = new StringBuilder (); - - // do not generate suggestions if the cursor is positioned in the middle of a word - bool areMidWord; - - if(idx == line.Count) { - // the cursor positioned at the very end of the line - areMidWord = false; - } - else { - // we are in the middle of a word if the cursor is over a letter/number - areMidWord = IsWordChar (line [idx]); - } - - // if we are in the middle of a word then there is no way to autocomplete that word - if(areMidWord) { - return null; - } - - // we are at the end of a word. Work out what has been typed so far - while(idx-- > 0) { - - if(IsWordChar(line [idx])) { - sb.Insert(0,(char)line [idx]); - } - else { - break; - } - } - return sb.ToString (); - } - - /// - /// Return true if the given symbol should be considered part of a word - /// and can be contained in matches. Base behaviour is to use - /// - /// - /// - public virtual bool IsWordChar (Rune rune) - { - return Char.IsLetterOrDigit ((char)rune); - } - } -} diff --git a/Terminal.Gui/Core/Autocomplete/Autocomplete.cs b/Terminal.Gui/Core/Autocomplete/Autocomplete.cs new file mode 100644 index 000000000..f351b8424 --- /dev/null +++ b/Terminal.Gui/Core/Autocomplete/Autocomplete.cs @@ -0,0 +1,642 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Text; +using Rune = System.Rune; + +namespace Terminal.Gui { + + /// + /// Renders an overlay on another view at a given point that allows selecting + /// from a range of 'autocomplete' options. + /// + public abstract class Autocomplete : IAutocomplete { + + private class Popup : View { + Autocomplete autocomplete; + + public Popup (Autocomplete autocomplete) + { + this.autocomplete = autocomplete; + CanFocus = true; + WantMousePositionReports = true; + } + + public override Rect Frame { + get => base.Frame; + set { + base.Frame = value; + X = value.X; + Y = value.Y; + Width = value.Width; + Height = value.Height; + } + } + + public override void Redraw (Rect bounds) + { + if (autocomplete.LastPopupPos == null) { + return; + } + + autocomplete.RenderOverlay ((Point)autocomplete.LastPopupPos); + } + + public override bool MouseEvent (MouseEvent mouseEvent) + { + return autocomplete.MouseEvent (mouseEvent); + } + } + + private View top, popup; + private bool closed; + int toRenderLength; + + private Point? LastPopupPos { get; set; } + + private ColorScheme colorScheme; + private View hostControl; + + /// + /// The host control to handle. + /// + public virtual View HostControl { + get => hostControl; + set { + hostControl = value; + top = hostControl.SuperView; + if (top != null) { + top.DrawContent += Top_DrawContent; + top.DrawContentComplete += Top_DrawContentComplete; + top.Removed += Top_Removed; + } + } + } + + private void Top_Removed (View obj) + { + Visible = false; + ManipulatePopup (); + } + + private void Top_DrawContentComplete (Rect obj) + { + ManipulatePopup (); + } + + private void Top_DrawContent (Rect obj) + { + if (!closed) { + ReopenSuggestions (); + } + ManipulatePopup (); + if (Visible) { + top.BringSubviewToFront (popup); + } + } + + private void ManipulatePopup () + { + if (Visible && popup == null) { + popup = new Popup (this) { + Frame = Rect.Empty + }; + top?.Add (popup); + } + + if (!Visible && popup != null) { + top.Remove (popup); + popup.Dispose (); + popup = null; + } + } + + /// + /// Gets or sets If the popup is displayed inside or outside the host limits. + /// + public bool PopupInsideContainer { get; set; } = true; + + /// + /// The maximum width of the autocomplete dropdown + /// + public virtual int MaxWidth { get; set; } = 10; + + /// + /// The maximum number of visible rows in the autocomplete dropdown to render + /// + public virtual int MaxHeight { get; set; } = 6; + + /// + /// True if the autocomplete should be considered open and visible + /// + public virtual bool Visible { get; set; } + + /// + /// The strings that form the current list of suggestions to render + /// based on what the user has typed so far. + /// + public virtual ReadOnlyCollection Suggestions { get; set; } = new ReadOnlyCollection (new string [0]); + + /// + /// The full set of all strings that can be suggested. + /// + /// + public virtual List AllSuggestions { get; set; } = new List (); + + /// + /// The currently selected index into that the user has highlighted + /// + public virtual int SelectedIdx { get; set; } + + /// + /// When more suggestions are available than can be rendered the user + /// can scroll down the dropdown list. This indicates how far down they + /// have gone + /// + public virtual int ScrollOffset { get; set; } + + /// + /// The colors to use to render the overlay. Accessing this property before + /// the Application has been initialized will cause an error + /// + public virtual ColorScheme ColorScheme { + get { + if (colorScheme == null) { + colorScheme = Colors.Menu; + } + return colorScheme; + } + set { + colorScheme = value; + } + } + + /// + /// The key that the user must press to accept the currently selected autocomplete suggestion + /// + public virtual Key SelectionKey { get; set; } = Key.Enter; + + /// + /// The key that the user can press to close the currently popped autocomplete menu + /// + public virtual Key CloseKey { get; set; } = Key.Esc; + + /// + /// The key that the user can press to reopen the currently popped autocomplete menu + /// + public virtual Key Reopen { get; set; } = Key.Space | Key.CtrlMask | Key.AltMask; + + /// + /// Renders the autocomplete dialog inside or outside the given at the + /// given point. + /// + /// + public virtual void RenderOverlay (Point renderAt) + { + if (!Visible || HostControl?.HasFocus == false || Suggestions.Count == 0) { + LastPopupPos = null; + Visible = false; + return; + } + + LastPopupPos = renderAt; + + int height, width; + + if (PopupInsideContainer) { + // don't overspill vertically + height = Math.Min (HostControl.Bounds.Height - renderAt.Y, MaxHeight); + // There is no space below, lets see if can popup on top + if (height < Suggestions.Count && HostControl.Bounds.Height - renderAt.Y >= height) { + // Verifies that the upper limit available is greater than the lower limit + if (renderAt.Y > HostControl.Bounds.Height - renderAt.Y) { + renderAt.Y = Math.Max (renderAt.Y - Math.Min (Suggestions.Count + 1, MaxHeight + 1), 0); + height = Math.Min (Math.Min (Suggestions.Count, MaxHeight), LastPopupPos.Value.Y - 1); + } + } + } else { + // don't overspill vertically + height = Math.Min (Math.Min (top.Bounds.Height - HostControl.Frame.Bottom, MaxHeight), Suggestions.Count); + // There is no space below, lets see if can popup on top + if (height < Suggestions.Count && HostControl.Frame.Y - top.Frame.Y >= height) { + // Verifies that the upper limit available is greater than the lower limit + if (HostControl.Frame.Y > top.Bounds.Height - HostControl.Frame.Y) { + renderAt.Y = Math.Max (HostControl.Frame.Y - Math.Min (Suggestions.Count, MaxHeight), 0); + height = Math.Min (Math.Min (Suggestions.Count, MaxHeight), HostControl.Frame.Y); + } + } else { + renderAt.Y = HostControl.Frame.Bottom; + } + } + + if (ScrollOffset > Suggestions.Count - height) { + ScrollOffset = 0; + } + var toRender = Suggestions.Skip (ScrollOffset).Take (height).ToArray (); + toRenderLength = toRender.Length; + + if (toRender.Length == 0) { + return; + } + + width = Math.Min (MaxWidth, toRender.Max (s => s.Length)); + + if (PopupInsideContainer) { + // don't overspill horizontally, let's see if can be displayed on the left + if (width > HostControl.Bounds.Width - renderAt.X) { + // Verifies that the left limit available is greater than the right limit + if (renderAt.X > HostControl.Bounds.Width - renderAt.X) { + renderAt.X -= Math.Min (width, LastPopupPos.Value.X); + width = Math.Min (width, LastPopupPos.Value.X); + } else { + width = Math.Min (width, HostControl.Bounds.Width - renderAt.X); + } + } + } else { + // don't overspill horizontally, let's see if can be displayed on the left + if (width > top.Bounds.Width - (renderAt.X + HostControl.Frame.X)) { + // Verifies that the left limit available is greater than the right limit + if (renderAt.X + HostControl.Frame.X > top.Bounds.Width - (renderAt.X + HostControl.Frame.X)) { + renderAt.X -= Math.Min (width, LastPopupPos.Value.X); + width = Math.Min (width, LastPopupPos.Value.X); + } else { + width = Math.Min (width, top.Bounds.Width - renderAt.X); + } + } + } + + if (PopupInsideContainer) { + popup.Frame = new Rect ( + new Point (HostControl.Frame.X + renderAt.X, HostControl.Frame.Y + renderAt.Y), + new Size (width, height)); + } else { + popup.Frame = new Rect ( + new Point (HostControl.Frame.X + renderAt.X, renderAt.Y), + new Size (width, height)); + } + + popup.Move (0, 0); + + for (int i = 0; i < toRender.Length; i++) { + + if (i == SelectedIdx - ScrollOffset) { + Application.Driver.SetAttribute (ColorScheme.Focus); + } else { + Application.Driver.SetAttribute (ColorScheme.Normal); + } + + popup.Move (0, i); + + var text = TextFormatter.ClipOrPad (toRender [i], width); + + Application.Driver.AddStr (text); + } + } + + /// + /// Updates to be a valid index within + /// + public virtual void EnsureSelectedIdxIsValid () + { + SelectedIdx = Math.Max (0, Math.Min (Suggestions.Count - 1, SelectedIdx)); + + // if user moved selection up off top of current scroll window + if (SelectedIdx < ScrollOffset) { + ScrollOffset = SelectedIdx; + } + + // if user moved selection down past bottom of current scroll window + while (toRenderLength > 0 && SelectedIdx >= ScrollOffset + toRenderLength) { + ScrollOffset++; + } + } + + /// + /// Handle key events before e.g. to make key events like + /// up/down apply to the autocomplete control instead of changing the cursor position in + /// the underlying text view. + /// + /// The key event. + /// trueif the key can be handled falseotherwise. + public virtual bool ProcessKey (KeyEvent kb) + { + if (IsWordChar ((char)kb.Key)) { + Visible = true; + closed = false; + } + + if (kb.Key == Reopen) { + return ReopenSuggestions (); + } + + if (closed || Suggestions.Count == 0) { + Visible = false; + return false; + } + + if (kb.Key == Key.CursorDown) { + MoveDown (); + return true; + } + + if (kb.Key == Key.CursorUp) { + MoveUp (); + return true; + } + + if (kb.Key == SelectionKey) { + return Select (); + } + + if (kb.Key == CloseKey) { + Close (); + return true; + } + + return false; + } + + /// + /// Handle mouse events before e.g. to make mouse events like + /// report/click apply to the autocomplete control instead of changing the cursor position in + /// the underlying text view. + /// + /// The mouse event. + /// If was called from the popup or from the host. + /// trueif the mouse can be handled falseotherwise. + public virtual bool MouseEvent (MouseEvent me, bool fromHost = false) + { + if (fromHost) { + GenerateSuggestions (); + if (Visible && Suggestions.Count == 0) { + Visible = false; + HostControl?.SetNeedsDisplay (); + return true; + } else if (!Visible && Suggestions.Count > 0) { + Visible = true; + HostControl?.SetNeedsDisplay (); + Application.UngrabMouse (); + return false; + } else { + // not in the popup + if (Visible && HostControl != null) { + Visible = false; + closed = false; + } + HostControl?.SetNeedsDisplay (); + } + return false; + } + + if (popup == null || Suggestions.Count == 0) { + ManipulatePopup (); + return false; + } + + if (me.Flags == MouseFlags.ReportMousePosition) { + RenderSelectedIdxByMouse (me); + return true; + } + + if (me.Flags == MouseFlags.Button1Clicked) { + SelectedIdx = me.Y - ScrollOffset; + return Select (); + } + + if (me.Flags == MouseFlags.WheeledDown) { + MoveDown (); + return true; + } + + if (me.Flags == MouseFlags.WheeledUp) { + MoveUp (); + return true; + } + + return false; + } + + /// + /// Render the current selection in the Autocomplete context menu by the mouse reporting. + /// + /// + protected void RenderSelectedIdxByMouse (MouseEvent me) + { + if (SelectedIdx != me.Y - ScrollOffset) { + SelectedIdx = me.Y - ScrollOffset; + if (LastPopupPos != null) { + RenderOverlay ((Point)LastPopupPos); + } + } + } + + /// + /// Clears + /// + public virtual void ClearSuggestions () + { + Suggestions = Enumerable.Empty ().ToList ().AsReadOnly (); + } + + + /// + /// Populates with all strings in that + /// match with the current cursor position/text in the + /// + public virtual void GenerateSuggestions () + { + // if there is nothing to pick from + if (AllSuggestions.Count == 0) { + ClearSuggestions (); + return; + } + + var currentWord = GetCurrentWord (); + + if (string.IsNullOrWhiteSpace (currentWord)) { + ClearSuggestions (); + } else { + Suggestions = AllSuggestions.Where (o => + o.StartsWith (currentWord, StringComparison.CurrentCultureIgnoreCase) && + !o.Equals (currentWord, StringComparison.CurrentCultureIgnoreCase) + ).ToList ().AsReadOnly (); + + EnsureSelectedIdxIsValid (); + } + } + + + /// + /// Return true if the given symbol should be considered part of a word + /// and can be contained in matches. Base behavior is to use + /// + /// + /// + public virtual bool IsWordChar (Rune rune) + { + return Char.IsLetterOrDigit ((char)rune); + } + + /// + /// Completes the autocomplete selection process. Called when user hits the . + /// + /// + protected bool Select () + { + if (SelectedIdx >= 0 && SelectedIdx < Suggestions.Count) { + var accepted = Suggestions [SelectedIdx]; + + return InsertSelection (accepted); + + } + + return false; + } + + /// + /// Called when the user confirms a selection at the current cursor location in + /// the . The string + /// is the full autocomplete word to be inserted. Typically a host will have to + /// remove some characters such that the string + /// completes the word instead of simply being appended. + /// + /// + /// True if the insertion was possible otherwise false + protected virtual bool InsertSelection (string accepted) + { + var typedSoFar = GetCurrentWord () ?? ""; + + if (typedSoFar.Length < accepted.Length) { + + // delete the text + for (int i = 0; i < typedSoFar.Length; i++) { + DeleteTextBackwards (); + } + + InsertText (accepted); + return true; + } + + return false; + } + + /// + /// Returns the currently selected word from the . + /// + /// When overriding this method views can make use of + /// + /// + /// + protected abstract string GetCurrentWord (); + + /// + /// + /// Given a of characters, returns the word which ends at + /// or null. Also returns null if the is positioned in the middle of a word. + /// + /// + /// Use this method to determine whether autocomplete should be shown when the cursor is at + /// a given point in a line and to get the word from which suggestions should be generated. + /// + /// + /// + /// + protected virtual string IdxToWord (List line, int idx) + { + StringBuilder sb = new StringBuilder (); + + // do not generate suggestions if the cursor is positioned in the middle of a word + bool areMidWord; + + if (idx == line.Count) { + // the cursor positioned at the very end of the line + areMidWord = false; + } else { + // we are in the middle of a word if the cursor is over a letter/number + areMidWord = IsWordChar (line [idx]); + } + + // if we are in the middle of a word then there is no way to autocomplete that word + if (areMidWord) { + return null; + } + + // we are at the end of a word. Work out what has been typed so far + while (idx-- > 0) { + + if (IsWordChar (line [idx])) { + sb.Insert (0, (char)line [idx]); + } else { + break; + } + } + return sb.ToString (); + } + + /// + /// Deletes the text backwards before insert the selected text in the . + /// + protected abstract void DeleteTextBackwards (); + + /// + /// Inser the selected text in the . + /// + /// + protected abstract void InsertText (string accepted); + + /// + /// Closes the Autocomplete context menu if it is showing and + /// + protected void Close () + { + ClearSuggestions (); + Visible = false; + closed = true; + HostControl?.SetNeedsDisplay (); + ManipulatePopup (); + } + + /// + /// Moves the selection in the Autocomplete context menu up one + /// + protected void MoveUp () + { + SelectedIdx--; + if (SelectedIdx < 0) { + SelectedIdx = Suggestions.Count - 1; + } + EnsureSelectedIdxIsValid (); + HostControl?.SetNeedsDisplay (); + } + + /// + /// Moves the selection in the Autocomplete context menu down one + /// + protected void MoveDown () + { + SelectedIdx++; + if (SelectedIdx > Suggestions.Count - 1) { + SelectedIdx = 0; + } + EnsureSelectedIdxIsValid (); + HostControl?.SetNeedsDisplay (); + } + + /// + /// Reopen the popup after it has been closed. + /// + /// + protected bool ReopenSuggestions () + { + GenerateSuggestions (); + if (Suggestions.Count > 0) { + Visible = true; + closed = false; + HostControl?.SetNeedsDisplay (); + return true; + } + return false; + } + } +} diff --git a/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs b/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs new file mode 100644 index 000000000..32e7046e7 --- /dev/null +++ b/Terminal.Gui/Core/Autocomplete/IAutocomplete.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using Rune = System.Rune; + +namespace Terminal.Gui { + + /// + /// Renders an overlay on another view at a given point that allows selecting + /// from a range of 'autocomplete' options. + /// + public interface IAutocomplete { + + /// + /// The host control that will use autocomplete. + /// + View HostControl { get; set; } + + /// + /// Gets or sets where the popup will be displayed. + /// + bool PopupInsideContainer { get; set; } + + /// + /// The maximum width of the autocomplete dropdown + /// + int MaxWidth { get; set; } + + /// + /// The maximum number of visible rows in the autocomplete dropdown to render + /// + int MaxHeight { get; set; } + + /// + /// True if the autocomplete should be considered open and visible + /// + bool Visible { get; set; } + + /// + /// The strings that form the current list of suggestions to render + /// based on what the user has typed so far. + /// + ReadOnlyCollection Suggestions { get; set; } + + /// + /// The full set of all strings that can be suggested. + /// + List AllSuggestions { get; set; } + + /// + /// The currently selected index into that the user has highlighted + /// + int SelectedIdx { get; set; } + + /// + /// The colors to use to render the overlay. Accessing this property before + /// the Application has been initialized will cause an error + /// + ColorScheme ColorScheme { get; set; } + + /// + /// The key that the user must press to accept the currently selected autocomplete suggestion + /// + Key SelectionKey { get; set; } + + /// + /// The key that the user can press to close the currently popped autocomplete menu + /// + Key CloseKey { get; set; } + + /// + /// The key that the user can press to reopen the currently popped autocomplete menu + /// + Key Reopen { get; set; } + + /// + /// Renders the autocomplete dialog inside the given at the + /// given point. + /// + /// + void RenderOverlay (Point renderAt); + + + /// + /// Handle key events before e.g. to make key events like + /// up/down apply to the autocomplete control instead of changing the cursor position in + /// the underlying text view. + /// + /// The key event. + /// trueif the key can be handled falseotherwise. + bool ProcessKey (KeyEvent kb); + + /// + /// Handle mouse events before e.g. to make mouse events like + /// report/click apply to the autocomplete control instead of changing the cursor position in + /// the underlying text view. + /// + /// The mouse event. + /// If was called from the popup or from the host. + /// trueif the mouse can be handled falseotherwise. + bool MouseEvent (MouseEvent me, bool fromHost = false); + + /// + /// Clears + /// + void ClearSuggestions (); + + /// + /// Populates with all strings in that + /// match with the current cursor position/text in the . + /// + void GenerateSuggestions (); + } +} diff --git a/Terminal.Gui/Core/Command.cs b/Terminal.Gui/Core/Command.cs new file mode 100644 index 000000000..54d6d65a4 --- /dev/null +++ b/Terminal.Gui/Core/Command.cs @@ -0,0 +1,388 @@ +// These classes use a keybinding system based on the design implemented in Scintilla.Net which is an MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs + +using System; + +namespace Terminal.Gui { + + /// + /// Actions which can be performed by the application or bound to keys in a control. + /// + public enum Command { + + /// + /// Moves the caret down one line. + /// + LineDown, + + /// + /// Extends the selection down one line. + /// + LineDownExtend, + + /// + /// Moves the caret down to the last child node of the branch that holds the current selection + /// + LineDownToLastBranch, + + /// + /// Scrolls down one line (without changing the selection). + /// + ScrollDown, + + // -------------------------------------------------------------------- + + /// + /// Moves the caret up one line. + /// + LineUp, + + /// + /// Extends the selection up one line. + /// + LineUpExtend, + + /// + /// Moves the caret up to the first child node of the branch that holds the current selection + /// + LineUpToFirstBranch, + + /// + /// Scrolls up one line (without changing the selection). + /// + ScrollUp, + + /// + /// Moves the selection left one by the minimum increment supported by the view e.g. single character, cell, item etc. + /// + Left, + + /// + /// Scrolls one character 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 character 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, + + /// + /// Deletes and copies to the clipboard the characters from the current position to the end of the line. + /// + CutToEndLine, + + /// + /// Deletes and copies 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 the page down. + /// + PageDown, + + /// + /// Move the page down increase selection area to cover revealed objects/characters. + /// + PageDownExtend, + + /// + /// Move the page up. + /// + PageUp, + + /// + /// Move the page up increase selection area to cover revealed objects/characters. + /// + PageUpExtend, + + /// + /// Moves to top begin. + /// + TopHome, + + /// + /// Extends the selection to the top begin. + /// + TopHomeExtend, + + /// + /// Moves to bottom end. + /// + BottomEnd, + + /// + /// Extends the selection to the bottom end. + /// + BottomEndExtend, + + /// + /// Open 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 any current 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 in the control + /// + SelectAll, + + /// + /// 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 toplevel. + /// + QuitToplevel, + + /// + /// Suspend a application (used on Linux). + /// + 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 Mdi). + /// + NextViewOrTop, + + /// + /// Moves focus to the next previous or toplevel (case of Mdi). + /// + PreviousViewOrTop, + + /// + /// Refresh the application. + /// + Refresh, + + /// + /// Toggles the extended selection. + /// + ToggleExtend, + + /// + /// Inserts a new line. + /// + NewLine, + + /// + /// Inserts a tab. + /// + Tab, + + /// + /// Inserts a shift tab. + /// + BackTab + } +} \ No newline at end of file diff --git a/Terminal.Gui/Core/Event.cs b/Terminal.Gui/Core/Event.cs index c85b7761a..181724b1c 100644 --- a/Terminal.Gui/Core/Event.cs +++ b/Terminal.Gui/Core/Event.cs @@ -237,7 +237,110 @@ namespace Terminal.Gui { /// 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. /// diff --git a/Terminal.Gui/Core/TextFormatter.cs b/Terminal.Gui/Core/TextFormatter.cs index c6f5f475a..66da40daf 100644 --- a/Terminal.Gui/Core/TextFormatter.cs +++ b/Terminal.Gui/Core/TextFormatter.cs @@ -123,6 +123,11 @@ namespace Terminal.Gui { Key hotKey; Size size; + /// + /// Event invoked when the is changed. + /// + public event Action HotKeyChanged; + /// /// The text to be displayed. This text is never modified. /// @@ -270,7 +275,16 @@ namespace Terminal.Gui { /// /// Gets the hotkey. Will be an upper case letter or digit. /// - public Key HotKey { get => hotKey; internal set => hotKey = value; } + public Key HotKey { + get => hotKey; + internal set { + if (hotKey != value) { + var oldKey = hotKey; + hotKey = value; + HotKeyChanged?.Invoke (oldKey); + } + } + } /// /// Specifies the mask to apply to the hotkey to tag it as the hotkey. The default value of 0x100000 causes @@ -304,7 +318,8 @@ namespace Terminal.Gui { if (NeedsFormat) { var shown_text = text; - if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out hotKey)) { + if (FindHotKey (text, HotKeySpecifier, true, out hotKeyPos, out Key newHotKey)) { + HotKey = newHotKey; shown_text = RemoveHotKeySpecifier (Text, hotKeyPos, HotKeySpecifier); shown_text = ReplaceHotKeyWithTag (shown_text, hotKeyPos); } diff --git a/Terminal.Gui/Core/Toplevel.cs b/Terminal.Gui/Core/Toplevel.cs index e191169fe..d32abb3de 100644 --- a/Terminal.Gui/Core/Toplevel.cs +++ b/Terminal.Gui/Core/Toplevel.cs @@ -197,6 +197,85 @@ namespace Terminal.Gui { void Initialize () { ColorScheme = Colors.TopLevel; + + // Things this view knows how to do + AddCommand (Command.QuitToplevel, () => { QuitToplevel (); return true; }); + AddCommand (Command.Suspend, () => { Driver.Suspend (); ; return true; }); + AddCommand (Command.NextView, () => { MoveNextView (); return true; }); + AddCommand (Command.PreviousView, () => { MovePreviousView (); return true; }); + AddCommand (Command.NextViewOrTop, () => { MoveNextViewOrTop (); return true; }); + AddCommand (Command.PreviousViewOrTop, () => { MovePreviousViewOrTop (); return true; }); + AddCommand (Command.Refresh, () => { Application.Refresh (); return true; }); + + // Default keybindings for this view + AddKeyBinding (Application.QuitKey, Command.QuitToplevel); + AddKeyBinding (Key.Z | Key.CtrlMask, Command.Suspend); + + AddKeyBinding (Key.Tab, Command.NextView); + + AddKeyBinding (Key.CursorRight, Command.NextView); + AddKeyBinding (Key.F | Key.CtrlMask, Command.NextView); + + AddKeyBinding (Key.CursorDown, Command.NextView); + AddKeyBinding (Key.I | Key.CtrlMask, Command.NextView); // Unix + + AddKeyBinding (Key.BackTab | Key.ShiftMask, Command.PreviousView); + AddKeyBinding (Key.CursorLeft, Command.PreviousView); + AddKeyBinding (Key.CursorUp, Command.PreviousView); + AddKeyBinding (Key.B | Key.CtrlMask, Command.PreviousView); + + AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextViewOrTop); + AddKeyBinding (Application.AlternateForwardKey, Command.NextViewOrTop); // Needed on Unix + + AddKeyBinding (Key.Tab | Key.ShiftMask | Key.CtrlMask, Command.PreviousViewOrTop); + AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousViewOrTop); // Needed on Unix + + AddKeyBinding (Key.L | Key.CtrlMask, Command.Refresh); + } + + /// + /// Invoked when the is changed. + /// + public event Action AlternateForwardKeyChanged; + + /// + /// Virtual method to invoke the event. + /// + /// + public virtual void OnAlternateForwardKeyChanged (Key oldKey) + { + ReplaceKeyBinding (oldKey, Application.AlternateForwardKey); + AlternateForwardKeyChanged?.Invoke (oldKey); + } + + /// + /// Invoked when the is changed. + /// + public event Action AlternateBackwardKeyChanged; + + /// + /// Virtual method to invoke the event. + /// + /// + public virtual void OnAlternateBackwardKeyChanged (Key oldKey) + { + ReplaceKeyBinding (oldKey, Application.AlternateBackwardKey); + AlternateBackwardKeyChanged?.Invoke (oldKey); + } + + /// + /// Invoked when the is changed. + /// + public event Action QuitKeyChanged; + + /// + /// Virtual method to invoke the event. + /// + /// + public virtual void OnQuitKeyChanged (Key oldKey) + { + ReplaceKeyBinding (oldKey, Application.QuitKey); + QuitKeyChanged?.Invoke (oldKey); } /// @@ -293,87 +372,86 @@ namespace Terminal.Gui { if (base.ProcessKey (keyEvent)) return true; - switch (ShortcutHelper.GetModifiersKey (keyEvent)) { - case Key k when k == Application.QuitKey: - // FIXED: stop current execution of this container - if (Application.MdiTop != null) { - Application.MdiTop.RequestStop (); - } else { - Application.RequestStop (); - } - break; - case Key.Z | Key.CtrlMask: - Driver.Suspend (); - 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 - case Key.F5: + if (keyEvent.Key == Key.F5) { Application.DebugDrawBounds = !Application.DebugDrawBounds; SetNeedsDisplay (); return true; -#endif - case Key.Tab: - case Key.CursorRight: - case Key.CursorDown: - case Key.I | Key.CtrlMask: // Unix - var old = GetDeepestFocusedSubview (Focused); - if (!FocusNext ()) - FocusNext (); - if (old != Focused && old != Focused?.Focused) { - old?.SetNeedsDisplay (); - Focused?.SetNeedsDisplay (); - } else { - FocusNearestView (SuperView?.TabIndexes, Direction.Forward); - } - return true; - case Key.BackTab | Key.ShiftMask: - case Key.CursorLeft: - case Key.CursorUp: - old = GetDeepestFocusedSubview (Focused); - if (!FocusPrev ()) - FocusPrev (); - if (old != Focused && old != Focused?.Focused) { - old?.SetNeedsDisplay (); - Focused?.SetNeedsDisplay (); - } else { - FocusNearestView (SuperView?.TabIndexes?.Reverse (), Direction.Backward); - } - return true; - case Key.Tab | Key.CtrlMask: - case Key key when key == Application.AlternateForwardKey: // Needed on Unix - if (Application.MdiTop == null) { - var top = Modal ? this : Application.Top; - top.FocusNext (); - if (top.Focused == null) { - top.FocusNext (); - } - top.SetNeedsDisplay (); - Application.EnsuresTopOnFront (); - } else { - MoveNext (); - } - return true; - case Key.Tab | Key.ShiftMask | Key.CtrlMask: - case Key key when key == Application.AlternateBackwardKey: // Needed on Unix - if (Application.MdiTop == null) { - var top = Modal ? this : Application.Top; - top.FocusPrev (); - if (top.Focused == null) { - top.FocusPrev (); - } - top.SetNeedsDisplay (); - Application.EnsuresTopOnFront (); - } else { - MovePrevious (); - } - return true; - case Key.L | Key.CtrlMask: - Application.Refresh (); - return true; } +#endif return false; } + private void MovePreviousViewOrTop () + { + if (Application.MdiTop == null) { + var top = Modal ? this : Application.Top; + top.FocusPrev (); + if (top.Focused == null) { + top.FocusPrev (); + } + top.SetNeedsDisplay (); + Application.EnsuresTopOnFront (); + } else { + MovePrevious (); + } + } + + private void MoveNextViewOrTop () + { + if (Application.MdiTop == null) { + var top = Modal ? this : Application.Top; + top.FocusNext (); + if (top.Focused == null) { + top.FocusNext (); + } + top.SetNeedsDisplay (); + Application.EnsuresTopOnFront (); + } else { + MoveNext (); + } + } + + private void MovePreviousView () + { + var old = GetDeepestFocusedSubview (Focused); + if (!FocusPrev ()) + FocusPrev (); + if (old != Focused && old != Focused?.Focused) { + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + } else { + FocusNearestView (SuperView?.TabIndexes?.Reverse (), Direction.Backward); + } + } + + private void MoveNextView () + { + var old = GetDeepestFocusedSubview (Focused); + if (!FocusNext ()) + FocusNext (); + if (old != Focused && old != Focused?.Focused) { + old?.SetNeedsDisplay (); + Focused?.SetNeedsDisplay (); + } else { + FocusNearestView (SuperView?.TabIndexes, Direction.Forward); + } + } + + private void QuitToplevel () + { + if (Application.MdiTop != null) { + Application.MdiTop.RequestStop (); + } else { + Application.RequestStop (); + } + } + /// public override bool ProcessColdKey (KeyEvent keyEvent) { diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 883ebe12b..2aef4ef49 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -178,6 +178,11 @@ namespace Terminal.Gui { /// public event Action VisibleChanged; + /// + /// Event invoked when the is changed. + /// + public event Action HotKeyChanged; + /// /// 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. /// @@ -250,6 +255,12 @@ namespace Terminal.Gui { // 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. /// @@ -709,6 +720,7 @@ namespace Terminal.Gui { TextDirection direction = TextDirection.LeftRight_TopBottom, Border border = null) { textFormatter = new TextFormatter (); + textFormatter.HotKeyChanged += TextFormatter_HotKeyChanged; TextDirection = direction; Border = border; if (Border != null) { @@ -736,6 +748,11 @@ namespace Terminal.Gui { Text = text; } + private void TextFormatter_HotKeyChanged (Key obj) + { + HotKeyChanged?.Invoke (obj); + } + /// /// Sets a flag indicating this view needs to be redisplayed because its state has changed. /// @@ -1315,7 +1332,7 @@ namespace Terminal.Gui { /// The color scheme for this view, if it is not defined, it returns the 's /// color scheme. /// - public ColorScheme ColorScheme { + public virtual ColorScheme ColorScheme { get { if (colorScheme == null) return SuperView?.ColorScheme; @@ -1426,6 +1443,10 @@ namespace Terminal.Gui { } } } + + // Invoke DrawContentCompleteEvent + OnDrawContentComplete (bounds); + ClearLayoutNeeded (); ClearNeedsDisplay (); } @@ -1455,6 +1476,31 @@ namespace Terminal.Gui { DrawContent?.Invoke (viewport); } + /// + /// Event invoked when the content area of the View is completed drawing. + /// + /// + /// + /// Will be invoked after any subviews removed with have been completed drawing. + /// + /// + /// Rect provides the view-relative rectangle describing the currently visible viewport into the . + /// + /// + public event Action DrawContentComplete; + + /// + /// Enables overrides after completed drawing infinitely scrolled content and/or a background behind removed controls. + /// + /// The view-relative rectangle describing the currently visible viewport into the + /// + /// This method will be called after any subviews removed with have been completed drawing. + /// + public virtual void OnDrawContentComplete (Rect viewport) + { + DrawContentComplete?.Invoke (viewport); + } + /// /// Causes the specified subview to have focus. /// @@ -1551,6 +1597,131 @@ namespace Terminal.Gui { return false; } + /// + /// Invokes any binding that is registered on this + /// and matches the + /// + /// The key event passed. + protected bool? InvokeKeybindings (KeyEvent keyEvent) + { + if (KeyBindings.ContainsKey (keyEvent.Key)) { + var command = 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})"); + } + + return CommandImplementations [command] (); + } + + return null; + } + + + /// + /// 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 + /// + /// + /// + public void AddKeyBinding (Key key, Command 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)) { + Command value = KeyBindings [fromKey]; + KeyBindings.Remove (fromKey); + KeyBindings [toKey] = value; + } + } + + /// + /// Checks if key combination already exist. + /// + /// The key to check. + /// true If the key already exist, falseotherwise. + public bool ContainsKeyBinding (Key key) + { + return KeyBindings.ContainsKey (key); + } + + /// + /// Removes all bound keys from the View making including the default + /// key combinations such as cursor navigation, scrolling etc + /// + 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 (Command command) + { + foreach(var kvp in KeyBindings.Where(kvp=>kvp.Value == 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; + } + /// public override bool ProcessHotKey (KeyEvent keyEvent) { @@ -2559,5 +2730,21 @@ namespace Terminal.Gui { { return Enabled ? ColorScheme.Normal : ColorScheme.Disabled; } + + /// + /// Get the top superview of a given . + /// + /// The superview view. + public View GetTopSuperView () + { + View top = Application.Top; + for (var v = this?.SuperView; v != null; v = v.SuperView) { + if (v != null) { + top = v; + } + } + + return top; + } } } diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index a4a608093..ae50617a8 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -57,7 +57,7 @@ namespace Terminal.Gui { /// public Button (ustring text, bool is_default = false) : base (text) { - Init (text, is_default); + Initialize (text, is_default); } /// @@ -89,7 +89,7 @@ namespace Terminal.Gui { public Button (int x, int y, ustring text, bool is_default) : base (new Rect (x, y, text.RuneCount + 4 + (is_default ? 2 : 0), 1), text) { - Init (text, is_default); + Initialize (text, is_default); } Rune _leftBracket; @@ -97,7 +97,7 @@ namespace Terminal.Gui { Rune _leftDefault; Rune _rightDefault; - void Init (ustring text, bool is_default) + void Initialize (ustring text, bool is_default) { TextAlignment = TextAlignment.Centered; @@ -112,6 +112,29 @@ namespace Terminal.Gui { this.is_default = is_default; this.text = text ?? string.Empty; Update (); + + HotKeyChanged += Button_HotKeyChanged; + + // 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); + } + } + + private void Button_HotKeyChanged (Key obj) + { + if (HotKey != Key.Null) { + if (ContainsKeyBinding (obj)) { + ReplaceKeyBinding (Key.Space | obj, Key.Space | HotKey); + } else { + AddKeyBinding (Key.Space | HotKey, Command.Accept); + } + } } /// @@ -171,16 +194,6 @@ namespace Terminal.Gui { SetNeedsDisplay (); } - bool CheckKey (KeyEvent key) - { - if (key.Key == (Key.AltMask | HotKey)) { - SetFocus (); - Clicked?.Invoke (); - return true; - } - return false; - } - /// public override bool ProcessHotKey (KeyEvent kb) { @@ -188,10 +201,7 @@ namespace Terminal.Gui { return false; } - if (kb.IsAlt) - return CheckKey (kb); - - return false; + return ExecuteHotKey (kb); } /// @@ -201,11 +211,7 @@ namespace Terminal.Gui { return false; } - if (IsDefault && kb.KeyValue == '\n') { - Clicked?.Invoke (); - return true; - } - return CheckKey (kb); + return ExecuteColdKey (kb); } /// @@ -215,14 +221,45 @@ namespace Terminal.Gui { return false; } - var c = kb.KeyValue; - if (c == '\n' || c == ' ' || kb.Key == HotKey) { - Clicked?.Invoke (); - return true; - } + 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 (); + } /// /// Clicked , raised when the user clicks the primary mouse button within the Bounds of this @@ -245,7 +282,7 @@ namespace Terminal.Gui { SetFocus (); SetNeedsDisplay (); } - Clicked?.Invoke (); + OnClicked (); } return true; diff --git a/Terminal.Gui/Views/Checkbox.cs b/Terminal.Gui/Views/Checkbox.cs index 97bf09e7d..b79737f1a 100644 --- a/Terminal.Gui/Views/Checkbox.cs +++ b/Terminal.Gui/Views/Checkbox.cs @@ -47,11 +47,7 @@ namespace Terminal.Gui { /// If set to true is checked. public CheckBox (ustring s, bool is_checked = false) : base () { - Checked = is_checked; - Text = s; - CanFocus = true; - Height = 1; - Width = s.RuneCount + 4; + Initialize (s, is_checked); } /// @@ -73,11 +69,24 @@ namespace Terminal.Gui { /// text length. /// public CheckBox (int x, int y, ustring s, bool is_checked) : base (new Rect (x, y, s.Length + 4, 1)) + { + Initialize (s, is_checked); + } + + void Initialize (ustring s, bool is_checked) { Checked = is_checked; Text = s; - CanFocus = true; + Height = 1; + Width = s.RuneCount + 4; + + // 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); } /// @@ -138,31 +147,34 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent kb) { - if (kb.KeyValue == ' ') { - var previousChecked = Checked; - Checked = !Checked; - OnToggled (previousChecked); - SetNeedsDisplay (); - return true; - } + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; + return base.ProcessKey (kb); } /// - public override bool ProcessHotKey (KeyEvent ke) + public override bool ProcessHotKey (KeyEvent kb) { - if (ke.Key == (Key.AltMask | HotKey)) { - SetFocus (); - var previousChecked = Checked; - Checked = !Checked; - OnToggled (previousChecked); - SetNeedsDisplay (); - return true; - } + if (kb.Key == (Key.AltMask | HotKey)) + return ToggleChecked (); return false; } + bool ToggleChecked () + { + if (!HasFocus) { + SetFocus (); + } + var previousChecked = Checked; + Checked = !Checked; + OnToggled (previousChecked); + SetNeedsDisplay (); + return true; + } + /// public override bool MouseEvent (MouseEvent me) { diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index aa8a82245..a63de4371 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -156,6 +156,32 @@ namespace Terminal.Gui { SetNeedsDisplay (); Search_Changed (Text); }; + + // Things this view knows how to do + AddCommand (Command.Accept, () => ActivateSelected ()); + AddCommand (Command.ToggleExpandCollapse, () => ExpandCollapse ()); + AddCommand (Command.Expand, () => Expand ()); + AddCommand (Command.Collapse, () => Collapse ()); + AddCommand (Command.LineDown, () => MoveDown ()); + AddCommand (Command.LineUp, () => MoveUp ()); + AddCommand (Command.PageDown, () => PageDown ()); + AddCommand (Command.PageUp, () => PageUp ()); + AddCommand (Command.TopHome, () => MoveHome ()); + AddCommand (Command.BottomEnd, () => MoveEnd ()); + AddCommand (Command.Cancel, () => CancelSelected ()); + 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); } private bool isShow = false; @@ -182,6 +208,11 @@ namespace Terminal.Gui { } } + /// + /// Gets the drop down list state, expanded or collapsed. + /// + public bool IsShow => isShow; + /// public new ColorScheme ColorScheme { get { @@ -318,89 +349,153 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent e) { - if (e.Key == Key.Enter && listview.SelectedItem > -1) { - Selected (); + var result = InvokeKeybindings (e); + if (result != null) + return (bool)result; + + return base.ProcessKey (e); + } + + bool UnixEmulation () + { + // Unix emulation + Reset (); + return true; + } + + bool CancelSelected () + { + search.SetFocus (); + search.Text = text = ""; + OnSelectedChanged (); + Collapse (); + return true; + } + + bool MoveEnd () + { + if (HasItems ()) { + listview.MoveEnd (); + } + return true; + } + + bool MoveHome () + { + if (HasItems ()) { + listview.MoveHome (); + } + return true; + } + + bool PageUp () + { + if (HasItems ()) { + listview.MovePageUp (); + } + return true; + } + + bool PageDown () + { + if (HasItems ()) { + listview.MovePageDown (); + } + return true; + } + + bool? MoveUp () + { + if (search.HasFocus) { // stop odd behavior on KeyUp when search has focus return true; } - if (e.Key == Key.F4 && (search.HasFocus || listview.HasFocus)) { - if (!isShow) { - SetSearchSet (); - isShow = true; - ShowList (); - FocusSelectedItem (); - } else { - isShow = false; - HideList (); - } - return true; - } - - if (e.Key == Key.CursorDown && search.HasFocus) { // jump to list - if (searchset?.Count > 0) { - listview.TabStop = true; - listview.SetFocus (); - SetValue (searchset [listview.SelectedItem]); - return true; - } else { - listview.TabStop = false; - SuperView.FocusNext (); - } - } - - if (e.Key == Key.CursorUp && search.HasFocus) { // stop odd behavior on KeyUp when search has focus - return true; - } - - if (e.Key == Key.CursorUp && listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search + if (listview.HasFocus && listview.SelectedItem == 0 && searchset?.Count > 0) // jump back to search { search.CursorPosition = search.Text.RuneCount; search.SetFocus (); return true; } + return null; + } - if (e.Key == Key.PageDown) { - if (listview.SelectedItem != -1) { - listview.MovePageDown (); + bool? MoveDown () + { + if (search.HasFocus) { // jump to list + if (searchset?.Count > 0) { + listview.TabStop = true; + listview.SetFocus (); + SetValue (searchset [listview.SelectedItem]); + } else { + listview.TabStop = false; + SuperView?.FocusNext (); } return true; } + return null; + } - if (e.Key == Key.PageUp) { - if (listview.SelectedItem != -1) { - listview.MovePageUp (); + /// + /// Toggles the expand/collapse state of the sublist in the combo box + /// + /// + bool ExpandCollapse () + { + if (search.HasFocus || listview.HasFocus) { + if (!isShow) { + return Expand (); + } else { + return Collapse (); } + } + return false; + } + + bool ActivateSelected () + { + if (HasItems ()) { + Selected (); return true; } + return false; + } - if (e.Key == Key.Home) { - if (listview.SelectedItem != -1) { - listview.MoveHome (); - } - return true; + bool HasItems () + { + return Source?.Count > 0; + } + + /// + /// Collapses the drop down list. Returns true if the state chagned or false + /// if it was already collapsed and no action was taken + /// + public virtual bool Collapse () + { + if (!isShow) { + return false; } - if (e.Key == Key.End) { - if (listview.SelectedItem != -1) { - listview.MoveEnd (); - } - return true; + isShow = false; + HideList (); + return true; + } + + /// + /// Expands the drop down list. Returns true if the state chagned or false + /// if it was already expanded and no action was taken + /// + public virtual bool Expand () + { + if (isShow) { + return false; } - if (e.Key == Key.Esc) { - search.SetFocus (); - search.Text = text = ""; - OnSelectedChanged (); - return true; - } + SetSearchSet (); + isShow = true; + ShowList (); + FocusSelectedItem (); - // Unix emulation - if (e.Key == (Key.U | Key.CtrlMask)) { - Reset (); - return true; - } - - return base.ProcessKey (e); + return true; } /// @@ -489,6 +584,7 @@ namespace Terminal.Gui { private void SetSearchSet () { + if (Source == null) { return; } // force deep copy foreach (var item in Source.ToList ()) { searchset.Add (item); diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 14178e572..3093eb59f 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -26,8 +26,8 @@ namespace Terminal.Gui { string longFormat; string shortFormat; - int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } - string Format { get { return isShort ? shortFormat : longFormat; } } + int fieldLen => isShort ? shortFieldLen : longFieldLen; + string format => isShort ? shortFormat : longFormat; /// /// DateChanged event, raised when the property has changed. @@ -49,8 +49,7 @@ namespace Terminal.Gui { /// 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, "") { - this.isShort = isShort; - Initialize (date); + Initialize (date, isShort); } /// @@ -64,20 +63,47 @@ namespace Terminal.Gui { /// public DateField (DateTime date) : base ("") { - this.isShort = true; - Width = FieldLen + 2; + Width = fieldLen + 2; Initialize (date); } - void Initialize (DateTime date) + void Initialize (DateTime date, bool isShort = false) { CultureInfo cultureInfo = CultureInfo.CurrentCulture; sepChar = cultureInfo.DateTimeFormat.DateSeparator; longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); shortFormat = GetShortFormat (longFormat); - CursorPosition = 1; + 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 (ustring e) @@ -129,8 +155,8 @@ namespace Terminal.Gui { var oldData = date; date = value; - this.Text = value.ToString (Format); - var args = new DateTimeEventArgs (oldData, value, Format); + this.Text = value.ToString (format); + var args = new DateTimeEventArgs (oldData, value, format); if (oldData != value) { OnDateChanged (args); } @@ -157,12 +183,20 @@ namespace Terminal.Gui { } } + /// + public override int CursorPosition { + get => base.CursorPosition; + set { + base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); + } + } + bool SetText (Rune key) { var text = TextModel.ToRunes (Text); var newText = text.GetRange (0, CursorPosition); newText.Add (key); - if (CursorPosition < FieldLen) + if (CursorPosition < fieldLen) newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); return SetText (ustring.Make (newText)); } @@ -174,7 +208,7 @@ namespace Terminal.Gui { } ustring [] vals = text.Split (ustring.Make (sepChar)); - ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar)); + ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar)); bool isValidDate = true; int idx = GetFormatIndex (frm, "y"); int year = Int32.Parse (vals [idx].ToString ()); @@ -204,7 +238,7 @@ namespace Terminal.Gui { day = Int32.Parse (vals [idx].ToString ()); string d = GetDate (month, day, year, frm); - if (!DateTime.TryParseExact (d, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || + if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) || !isValidDate) return false; Date = result; @@ -238,7 +272,7 @@ namespace Terminal.Gui { ustring GetDate (ustring text) { ustring [] vals = text.Split (ustring.Make (sepChar)); - ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar)); + ustring [] frm = ustring.Make (format).Split (ustring.Make (sepChar)); ustring [] date = { null, null, null }; for (int i = 0; i < frm.Length; i++) { @@ -274,7 +308,7 @@ namespace Terminal.Gui { void IncCursorPosition () { - if (CursorPosition == FieldLen) + if (CursorPosition == fieldLen) return; if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) CursorPosition++; @@ -297,60 +331,69 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent kb) { - switch (kb.Key) { - case Key.DeleteChar: - case Key.D | Key.CtrlMask: - if (ReadOnly) - return true; + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; - SetText ('0'); - break; + // Ignore non-numeric characters. + if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) + return false; - case Key.Delete: - case Key.Backspace: - if (ReadOnly) - return true; - - SetText ('0'); - DecCursorPosition (); - break; - - // Home, C-A - case Key.Home: - case Key.A | Key.CtrlMask: - CursorPosition = 1; - break; - - case Key.CursorLeft: - case Key.B | Key.CtrlMask: - DecCursorPosition (); - break; - - case Key.End: - case Key.E | Key.CtrlMask: // End - CursorPosition = FieldLen; - break; - - case Key.CursorRight: - case Key.F | Key.CtrlMask: - IncCursorPosition (); - break; - - default: - // Ignore non-numeric characters. - if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9')) - return false; - - if (ReadOnly) - return true; - - if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) - IncCursorPosition (); + if (ReadOnly) return true; - } + + if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) + IncCursorPosition (); + return true; } + bool MoveRight () + { + IncCursorPosition (); + return true; + } + + 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 ('0'); + DecCursorPosition (); + return; + } + + /// + public override void DeleteCharRight () + { + if (ReadOnly) + return; + + SetText ('0'); + return; + } + /// public override bool MouseEvent (MouseEvent ev) { @@ -360,8 +403,8 @@ namespace Terminal.Gui { SetFocus (); var point = ev.X; - if (point > FieldLen) - point = FieldLen; + if (point > fieldLen) + point = fieldLen; if (point < 1) point = 1; CursorPosition = point; @@ -386,7 +429,7 @@ namespace Terminal.Gui { /// /// The old or value. /// - public T OldValue {get;} + public T OldValue { get; } /// /// The new or value. diff --git a/Terminal.Gui/Views/GraphView.cs b/Terminal.Gui/Views/GraphView.cs index f192da841..80cb9702e 100644 --- a/Terminal.Gui/Views/GraphView.cs +++ b/Terminal.Gui/Views/GraphView.cs @@ -74,6 +74,23 @@ namespace Terminal.Gui { AxisX = new HorizontalAxis (); AxisY = new VerticalAxis (); + + // Things this view knows how to do + AddCommand (Command.ScrollUp, () => { Scroll (0, CellSize.Y); return true; }); + AddCommand (Command.ScrollDown, () => { Scroll (0, -CellSize.Y); return true; }); + AddCommand (Command.ScrollRight, () => { Scroll (CellSize.X, 0); return true; }); + AddCommand (Command.ScrollLeft, () => { Scroll (-CellSize.X, 0); return true; }); + 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); + + // Not bound by default (preserves backwards compatibility) + //AddKeyBinding (Key.PageUp, Command.PageUp); + //AddKeyBinding (Key.PageDown, Command.PageDown); } /// @@ -228,48 +245,37 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - //&& Focused == tabsBar - if (HasFocus && CanFocus) { - switch (keyEvent.Key) { - - case Key.CursorLeft: - Scroll (-CellSize.X, 0); - return true; - case Key.CursorLeft | Key.CtrlMask: - Scroll (-CellSize.X * 5, 0); - return true; - case Key.CursorRight: - Scroll (CellSize.X, 0); - return true; - case Key.CursorRight | Key.CtrlMask: - Scroll (CellSize.X * 5, 0); - return true; - case Key.CursorDown: - Scroll (0, -CellSize.Y); - return true; - case Key.CursorDown | Key.CtrlMask: - Scroll (0, -CellSize.Y * 5); - return true; - case Key.CursorUp: - Scroll (0, CellSize.Y); - return true; - case Key.CursorUp | Key.CtrlMask: - Scroll (0, CellSize.Y * 5); - return true; - } + var result = InvokeKeybindings (keyEvent); + if (result != null) + return (bool)result; } return base.ProcessKey (keyEvent); } + /// + /// Scrolls the graph up 1 page + /// + public void PageUp() + { + Scroll (0, CellSize.Y * Bounds.Height); + } + + /// + /// Scrolls the graph down 1 page + /// + public void PageDown() + { + Scroll(0, -1 * CellSize.Y * Bounds.Height); + } /// /// Scrolls the view by a given number of units in graph space. /// See to translate this into rows/cols /// /// /// - private void Scroll (float offsetX, float offsetY) + public void Scroll (float offsetX, float offsetY) { ScrollOffset = new PointF ( ScrollOffset.X + offsetX, diff --git a/Terminal.Gui/Views/HexView.cs b/Terminal.Gui/Views/HexView.cs index fd086c3b2..e92a17041 100644 --- a/Terminal.Gui/Views/HexView.cs +++ b/Terminal.Gui/Views/HexView.cs @@ -58,6 +58,41 @@ namespace Terminal.Gui { 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); } /// @@ -390,76 +425,49 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - switch (keyEvent.Key) { - case Key.CursorLeft: - return MoveLeft (); - case Key.CursorRight: - return MoveRight (); - case Key.CursorDown: - return MoveDown (bytesPerLine); - case Key.CursorUp: - return MoveUp (bytesPerLine); - case Key.Enter: - return ToggleSide (); - case ((int)'v' + Key.AltMask): - case Key.PageUp: - return MoveUp (bytesPerLine * Frame.Height); - case Key.V | Key.CtrlMask: - case Key.PageDown: - return MoveDown (bytesPerLine * Frame.Height); - case Key.Home: - return MoveHome (); - case Key.End: - return MoveEnd (); - case Key.CursorLeft | Key.CtrlMask: - return MoveStartOfLine (); - case Key.CursorRight | Key.CtrlMask: - return MoveEndOfLine (); - case Key.CursorUp | Key.CtrlMask: - return MoveUp (bytesPerLine * ((int)(position - displayStart) / bytesPerLine)); - case Key.CursorDown | Key.CtrlMask: - return MoveDown (bytesPerLine * (Frame.Height - 1 - ((int)(position - displayStart) / bytesPerLine))); - default: - if (!AllowEdits) + 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; - // 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 KeyValuePair (position, edits [position])); - } else { - b = (byte)(b & 0xf0 | value); - edits [position] = b; - OnEdited (new KeyValuePair (position, edits [position])); - MoveRight (); - } - return true; - } 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 KeyValuePair (position, edits [position])); + } else { + b = (byte)(b & 0xf0 | value); + edits [position] = b; + OnEdited (new KeyValuePair (position, edits [position])); + MoveRight (); + } + return true; + } else + return false; } /// diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index df7bcd7af..7ce153ad5 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -317,6 +317,38 @@ namespace Terminal.Gui { { Source = source; CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.LineUp, () => MoveUp ()); + AddCommand (Command.LineDown, () => MoveDown ()); + AddCommand (Command.ScrollUp, () => ScrollUp (1)); + AddCommand (Command.ScrollDown, () => ScrollDown (1)); + AddCommand (Command.PageUp, () => MovePageUp ()); + AddCommand (Command.PageDown, () => MovePageDown ()); + AddCommand (Command.TopHome, () => MoveHome ()); + AddCommand (Command.BottomEnd, () => MoveEnd ()); + AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ()); + AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ()); + + // Default keybindings for all ListViews + AddKeyBinding (Key.CursorUp,Command.LineUp); + AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); + + AddKeyBinding (Key.CursorDown, Command.LineDown); + AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); + + AddKeyBinding(Key.PageUp,Command.PageUp); + + AddKeyBinding (Key.PageDown, Command.PageDown); + AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); + + AddKeyBinding (Key.Home, Command.TopHome); + + AddKeyBinding (Key.End, Command.BottomEnd); + + AddKeyBinding (Key.Enter, Command.OpenSelectedItem); + + AddKeyBinding (Key.Space, Command.ToggleChecked); } /// @@ -383,42 +415,11 @@ namespace Terminal.Gui { if (source == null) return base.ProcessKey (kb); - switch (kb.Key) { - case Key.CursorUp: - case Key.P | Key.CtrlMask: - return MoveUp (); + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; - case Key.CursorDown: - case Key.N | Key.CtrlMask: - return MoveDown (); - - case Key.V | Key.CtrlMask: - case Key.PageDown: - return MovePageDown (); - - case Key.PageUp: - return MovePageUp (); - - case Key.Space: - if (MarkUnmarkRow ()) - return true; - else - break; - - case Key.Enter: - return OnOpenSelectedItem (); - - case Key.End: - return MoveEnd (); - - case Key.Home: - return MoveHome (); - - default: - return false; - } - - return true; + return false; } /// @@ -602,40 +603,44 @@ namespace Terminal.Gui { /// Scrolls the view down. /// /// Number of lines to scroll down. - public virtual void ScrollDown (int lines) + public virtual bool ScrollDown (int lines) { top = Math.Max (Math.Min (top + lines, source.Count - 1), 0); SetNeedsDisplay (); + return true; } /// /// Scrolls the view up. /// /// Number of lines to scroll up. - public virtual void ScrollUp (int lines) + public virtual bool ScrollUp (int lines) { top = Math.Max (top - lines, 0); SetNeedsDisplay (); + return true; } /// /// Scrolls the view right. /// /// Number of columns to scroll right. - public virtual void ScrollRight (int cols) + public virtual bool ScrollRight (int cols) { left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0); SetNeedsDisplay (); + return true; } /// /// Scrolls the view left. /// /// Number of columns to scroll left. - public virtual void ScrollLeft (int cols) + public virtual bool ScrollLeft (int cols) { left = Math.Max (left - cols, 0); SetNeedsDisplay (); + return true; } int lastSelectedItem = -1; diff --git a/Terminal.Gui/Views/Menu.cs b/Terminal.Gui/Views/Menu.cs index bf073e4d1..9dfe567c4 100644 --- a/Terminal.Gui/Views/Menu.cs +++ b/Terminal.Gui/Views/Menu.cs @@ -413,6 +413,25 @@ namespace Terminal.Gui { WantMousePositionReports = host.WantMousePositionReports; } + // 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 + && current > -1 && current < this.barItems.Children.Length && this.barItems.Children [current].IsFromSubMenu) + ? true : false); 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); } internal Attribute DetermineColorSchemeFor (MenuItem item, int index) @@ -550,40 +569,21 @@ namespace Terminal.Gui { public override bool ProcessKey (KeyEvent kb) { - switch (kb.Key) { - case Key.Tab: - host.CleanUp (); - return true; - case Key.CursorUp: - return MoveUp (); - case Key.CursorDown: - return MoveDown (); - case Key.CursorLeft: - host.PreviousMenu (true); - return true; - case Key.CursorRight: - host.NextMenu (barItems.IsTopLevel || (barItems.Children != null && current > -1 && current < barItems.Children.Length && barItems.Children [current].IsFromSubMenu) ? true : false); - return true; - case Key.Esc: - CloseAllMenus (); - return true; - case Key.Enter: - RunSelected (); - return true; - default: - // TODO: rune-ify - if (barItems.Children != null && Char.IsLetterOrDigit ((char)kb.KeyValue)) { - var x = Char.ToUpper ((char)kb.KeyValue); - foreach (var item in barItems.Children) { - if (item == null) continue; - if (item.IsEnabled () && item.HotKey == x) { - host.CloseMenu (); - Run (item.Action); - return true; - } + 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); + foreach (var item in barItems.Children) { + if (item == null) continue; + if (item.IsEnabled () && item.HotKey == x) { + host.CloseMenu (); + Run (item.Action); + return true; } } - break; } return false; } @@ -832,6 +832,20 @@ namespace Terminal.Gui { ColorScheme = Colors.Menu; WantMousePositionReports = true; IsMenuOpen = false; + + // 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 openedByAltKey; @@ -1491,48 +1505,30 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent kb) { - switch (kb.Key) { - case Key.CursorLeft: - MoveLeft (); + if (InvokeKeybindings (kb) == true) return true; - case Key.CursorRight: - MoveRight (); - 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); - case Key.Esc: - case Key.C | Key.CtrlMask: - CloseMenuBar (); - return true; + if (selected == -1 || Menus [selected].IsTopLevel) + return false; - case Key.CursorDown: - case Key.Enter: - ProcessMenu (selected, Menus [selected]); - return true; - - default: - 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 ('_'); - if (p != -1 && p + 1 < mi.Title.RuneCount) { - if (mi.Title [p + 1] == c) { - Selected (mi); - return true; - } + foreach (var mi in Menus [selected].Children) { + if (mi == null) + continue; + int p = mi.Title.IndexOf ('_'); + if (p != -1 && p + 1 < mi.Title.RuneCount) { + if (mi.Title [p + 1] == c) { + Selected (mi); + return true; } } } - - return false; } + + return false; } void CloseMenuBar () diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs index 2e4a2fe66..e215af55b 100644 --- a/Terminal.Gui/Views/RadioGroup.cs +++ b/Terminal.Gui/Views/RadioGroup.cs @@ -14,20 +14,6 @@ namespace Terminal.Gui { int horizontalSpace = 2; List<(int pos, int length)> horizontal; - void Init (Rect rect, ustring [] radioLabels, int selected) - { - if (radioLabels == null) { - this.radioLabels = new List (); - } else { - this.radioLabels = radioLabels.ToList (); - } - - this.selected = selected; - SetWidthHeight (this.radioLabels); - CanFocus = true; - } - - /// /// Initializes a new instance of the class using layout. /// @@ -40,7 +26,7 @@ namespace Terminal.Gui { /// The index of the item to be selected, the value is clamped to the number of items. public RadioGroup (ustring [] radioLabels, int selected = 0) : base () { - Init (Rect.Empty, radioLabels, selected); + Initialize (radioLabels, selected); } /// @@ -51,7 +37,7 @@ namespace Terminal.Gui { /// The index of item to be selected, the value is clamped to the number of items. public RadioGroup (Rect rect, ustring [] radioLabels, int selected = 0) : base (rect) { - Init (rect, radioLabels, selected); + Initialize (radioLabels, selected); } /// @@ -61,11 +47,38 @@ namespace Terminal.Gui { /// 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. + /// The item to be selected, the value is clamped to the number of items. public RadioGroup (int x, int y, ustring [] radioLabels, int selected = 0) : this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected) { } + void Initialize (ustring [] radioLabels, int selected) + { + if (radioLabels == null) { + this.radioLabels = new List (); + } else { + this.radioLabels = radioLabels.ToList (); + } + + this.selected = selected; + SetWidthHeight (this.radioLabels); + CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.LineUp, () => { MoveUp (); return true; }); + AddCommand (Command.LineDown, () => { MoveDown (); return true; }); + AddCommand (Command.TopHome, () => { MoveHome (); return true; }); + AddCommand (Command.BottomEnd, () => { MoveEnd (); return true; }); + AddCommand (Command.Accept, () => { SelectItem (); return true; }); + + // Default keybindings for this view + 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); + } + /// /// Gets or sets the for this . /// @@ -215,33 +228,6 @@ namespace Terminal.Gui { } } - // TODO: Make this a global class - /// - /// Event arguments for the SelectedItemChagned event. - /// - public class SelectedItemChangedArgs : EventArgs { - /// - /// Gets the index of the item that was previously selected. -1 if there was no previous selection. - /// - public int PreviousSelectedItem { get; } - - /// - /// Gets the index of the item that is now selected. -1 if there is no selection. - /// - public int SelectedItem { get; } - - /// - /// Initializes a new class. - /// - /// - /// - public SelectedItemChangedArgs (int selectedItem, int previousSelectedItem) - { - PreviousSelectedItem = previousSelectedItem; - SelectedItem = selectedItem; - } - } - /// /// Invoked when the selected radio label has changed. /// @@ -311,28 +297,50 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent kb) { - switch (kb.Key) { - case Key.CursorUp: - if (cursor > 0) { - cursor--; - SetNeedsDisplay (); - return true; - } - break; - case Key.CursorDown: - if (cursor + 1 < radioLabels.Count) { - cursor++; - SetNeedsDisplay (); - return true; - } - break; - case Key.Space: - SelectedItem = cursor; - return true; - } + 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 (); + } + } + + void MoveUp () + { + if (cursor > 0) { + cursor--; + SetNeedsDisplay (); + } else if (radioLabels.Count - 1 > 0) { + cursor = radioLabels.Count - 1; + SetNeedsDisplay (); + } + } + /// public override bool MouseEvent (MouseEvent me) { @@ -379,4 +387,30 @@ namespace Terminal.Gui { /// Horizontal } + + /// + /// Event arguments for the SelectedItemChagned event. + /// + public class SelectedItemChangedArgs : EventArgs { + /// + /// Gets the index of the item that was previously selected. -1 if there was no previous selection. + /// + public int PreviousSelectedItem { get; } + + /// + /// Gets the index of the item that is now selected. -1 if there is no selection. + /// + public int SelectedItem { get; } + + /// + /// Initializes a new class. + /// + /// + /// + public SelectedItemChangedArgs (int selectedItem, int previousSelectedItem) + { + PreviousSelectedItem = previousSelectedItem; + SelectedItem = selectedItem; + } + } } diff --git a/Terminal.Gui/Views/ScrollView.cs b/Terminal.Gui/Views/ScrollView.cs index 943d2dbcb..a77a8f2aa 100644 --- a/Terminal.Gui/Views/ScrollView.cs +++ b/Terminal.Gui/Views/ScrollView.cs @@ -39,7 +39,7 @@ namespace Terminal.Gui { /// public ScrollView (Rect frame) : base (frame) { - Init (frame); + Initialize (frame); } @@ -48,10 +48,10 @@ namespace Terminal.Gui { /// public ScrollView () : base () { - Init (new Rect (0, 0, 0, 0)); + Initialize (Rect.Empty); } - void Init (Rect frame) + void Initialize (Rect frame) { contentView = new View (frame); vertical = new ScrollBarView (1, 0, isVertical: true) { @@ -74,6 +74,8 @@ namespace Terminal.Gui { ContentOffset = new Point (horizontal.Position, ContentOffset.Y); }; horizontal.Host = this; + vertical.OtherScrollBarView = horizontal; + horizontal.OtherScrollBarView = vertical; base.Add (contentView); CanFocus = true; @@ -81,6 +83,39 @@ namespace Terminal.Gui { MouseLeave += View_MouseLeave; contentView.MouseEnter += View_MouseEnter; contentView.MouseLeave += View_MouseLeave; + + // Things this view knows how to do + AddCommand (Command.ScrollUp, () => ScrollUp (1)); + AddCommand (Command.ScrollDown, () => ScrollDown (1)); + AddCommand (Command.ScrollLeft, () => ScrollLeft (1)); + AddCommand (Command.ScrollRight, () => ScrollRight (1)); + AddCommand (Command.PageUp, () => ScrollUp (Bounds.Height)); + AddCommand (Command.PageDown, () => ScrollDown (Bounds.Height)); + AddCommand (Command.PageLeft, () => ScrollLeft (Bounds.Width)); + AddCommand (Command.PageRight, () => ScrollRight (Bounds.Width)); + AddCommand (Command.TopHome, () => ScrollUp (contentSize.Height)); + AddCommand (Command.BottomEnd, () => ScrollDown (contentSize.Height)); + AddCommand (Command.LeftHome, () => ScrollLeft (contentSize.Width)); + 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); + + AddKeyBinding (Key.PageUp, Command.PageUp); + AddKeyBinding ((Key)'v' | Key.AltMask, Command.PageUp); + + AddKeyBinding (Key.PageDown, Command.PageDown); + AddKeyBinding (Key.V | Key.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); } Size contentSize; @@ -451,33 +486,10 @@ namespace Terminal.Gui { if (base.ProcessKey (kb)) return true; - switch (kb.Key) { - case Key.CursorUp: - return ScrollUp (1); - case (Key)'v' | Key.AltMask: - case Key.PageUp: - return ScrollUp (Bounds.Height); + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; - case Key.V | Key.CtrlMask: - case Key.PageDown: - return ScrollDown (Bounds.Height); - - case Key.CursorDown: - return ScrollDown (1); - - case Key.CursorLeft: - return ScrollLeft (1); - - case Key.CursorRight: - return ScrollRight (1); - - case Key.Home: - return ScrollUp (contentSize.Height); - - case Key.End: - return ScrollDown (contentSize.Height); - - } return false; } diff --git a/Terminal.Gui/Views/TabView.cs b/Terminal.Gui/Views/TabView.cs index 2b925d010..d99db86e4 100644 --- a/Terminal.Gui/Views/TabView.cs +++ b/Terminal.Gui/Views/TabView.cs @@ -110,6 +110,19 @@ namespace Terminal.Gui { base.Add (tabsBar); base.Add (contentView); + + // Things this view knows how to do + AddCommand (Command.Left, () => { SwitchTabBy (-1); return true; }); + AddCommand (Command.Right, () => { SwitchTabBy (1); return true; }); + AddCommand (Command.LeftHome, () => { SelectedTab = Tabs.FirstOrDefault (); return true; }); + 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); } /// @@ -179,7 +192,7 @@ namespace Terminal.Gui { if (Tabs.Any ()) { tabsBar.Redraw (tabsBar.Bounds); - contentView.SetNeedsDisplay(); + contentView.SetNeedsDisplay (); contentView.Redraw (contentView.Bounds); } } @@ -216,21 +229,9 @@ namespace Terminal.Gui { public override bool ProcessKey (KeyEvent keyEvent) { if (HasFocus && CanFocus && Focused == tabsBar) { - switch (keyEvent.Key) { - - case Key.CursorLeft: - SwitchTabBy (-1); - return true; - case Key.CursorRight: - SwitchTabBy (1); - return true; - case Key.Home: - SelectedTab = Tabs.FirstOrDefault (); - return true; - case Key.End: - SelectedTab = Tabs.LastOrDefault (); - return true; - } + var result = InvokeKeybindings (keyEvent); + if (result != null) + return (bool)result; } return base.ProcessKey (keyEvent); @@ -673,7 +674,7 @@ namespace Terminal.Gui { public override bool MouseEvent (MouseEvent me) { - if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && + if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) && !me.Flags.HasFlag (MouseFlags.Button1TripleClicked)) return false; diff --git a/Terminal.Gui/Views/TableView.cs b/Terminal.Gui/Views/TableView.cs index 0a11ff10f..8d51ddcf5 100644 --- a/Terminal.Gui/Views/TableView.cs +++ b/Terminal.Gui/Views/TableView.cs @@ -58,6 +58,7 @@ namespace Terminal.Gui { private int selectedColumn; private DataTable table; private TableStyle style = new TableStyle (); + private Key cellActivationKey = Key.Enter; /// /// The default maximum cell width for and @@ -171,7 +172,15 @@ namespace Terminal.Gui { /// /// The key which when pressed should trigger event. Defaults to Enter. /// - public Key CellActivationKey { get; set; } = Key.Enter; + public Key CellActivationKey { + get => cellActivationKey; + set { + if (cellActivationKey != value) { + ReplaceKeyBinding (cellActivationKey, value); + cellActivationKey = value; + } + } + } /// /// Initialzies a class using layout. @@ -188,32 +197,84 @@ namespace Terminal.Gui { public TableView () : base () { CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; }); + AddCommand (Command.Left, () => { ChangeSelectionByOffset (-1, 0, false); return true; }); + AddCommand (Command.LineUp, () => { ChangeSelectionByOffset (0, -1, false); return true; }); + AddCommand (Command.LineDown, () => { ChangeSelectionByOffset (0, 1, false); return true; }); + AddCommand (Command.PageUp, () => { PageUp (false); return true; }); + AddCommand (Command.PageDown, () => { PageDown (false); return true; }); + AddCommand (Command.LeftHome, () => { ChangeSelectionToStartOfRow (false); return true; }); + AddCommand (Command.RightEnd, () => { ChangeSelectionToEndOfRow (false); return true; }); + AddCommand (Command.TopHome, () => { ChangeSelectionToStartOfTable(false); return true; }); + AddCommand (Command.BottomEnd, () => { ChangeSelectionToEndOfTable (false); return true; }); + + AddCommand (Command.RightExtend, () => { ChangeSelectionByOffset (1, 0, true); return true; }); + AddCommand (Command.LeftExtend, () => { ChangeSelectionByOffset (-1, 0, true); return true; }); + AddCommand (Command.LineUpExtend, () => { ChangeSelectionByOffset (0, -1, true); return true; }); + AddCommand (Command.LineDownExtend, () => { ChangeSelectionByOffset (0, 1, true); return true; }); + AddCommand (Command.PageUpExtend, () => { PageUp (true); return true; }); + AddCommand (Command.PageDownExtend, () => { PageDown (true); return true; }); + AddCommand (Command.LeftHomeExtend, () => { ChangeSelectionToStartOfRow (true); return true; }); + AddCommand (Command.RightEndExtend, () => { ChangeSelectionToEndOfRow (true); return true; }); + AddCommand (Command.TopHomeExtend, () => { ChangeSelectionToStartOfTable (true); return true; }); + AddCommand (Command.BottomEndExtend, () => { ChangeSelectionToEndOfTable (true); return true; }); + + AddCommand (Command.SelectAll, () => { SelectAll(); return true; }); + AddCommand (Command.Accept, () => { new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow); return true; }); + + // Default keybindings for this view + AddKeyBinding (Key.CursorLeft, Command.Left); + AddKeyBinding (Key.CursorRight, Command.Right); + AddKeyBinding (Key.CursorUp, Command.LineUp); + AddKeyBinding (Key.CursorDown, Command.LineDown); + AddKeyBinding (Key.PageUp, Command.PageUp); + AddKeyBinding (Key.PageDown, Command.PageDown); + AddKeyBinding (Key.Home, Command.LeftHome); + AddKeyBinding (Key.End, Command.RightEnd); + AddKeyBinding (Key.Home | Key.CtrlMask, Command.TopHome); + AddKeyBinding (Key.End | Key.CtrlMask, Command.BottomEnd); + + AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); + AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); + AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); + AddKeyBinding (Key.CursorDown| Key.ShiftMask, Command.LineDownExtend); + AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); + AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); + AddKeyBinding (Key.Home | Key.ShiftMask, Command.LeftHomeExtend); + AddKeyBinding (Key.End | Key.ShiftMask, Command.RightEndExtend); + AddKeyBinding (Key.Home | Key.CtrlMask | Key.ShiftMask, Command.TopHomeExtend); + AddKeyBinding (Key.End | Key.CtrlMask | Key.ShiftMask, Command.BottomEndExtend); + + AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll); + AddKeyBinding (CellActivationKey, Command.Accept); } /// public override void Redraw (Rect bounds) - { - Move (0, 0); - var frame = Frame; + { + Move (0, 0); + var frame = Frame; - // What columns to render at what X offset in viewport - var columnsToRender = CalculateViewport (bounds).ToArray (); + // What columns to render at what X offset in viewport + var columnsToRender = CalculateViewport (bounds).ToArray (); - Driver.SetAttribute (GetNormalColor ()); + Driver.SetAttribute (GetNormalColor ()); - //invalidate current row (prevents scrolling around leaving old characters in the frame - Driver.AddStr (new string (' ', bounds.Width)); + //invalidate current row (prevents scrolling around leaving old characters in the frame + Driver.AddStr (new string (' ', bounds.Width)); - int line = 0; + int line = 0; - if (ShouldRenderHeaders ()) { - // Render something like: - /* - ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ - │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ - └────────────────────┴──────────┴───────────┴──────────────┴─────────┘ - */ - if (Style.ShowHorizontalHeaderOverline) { + if (ShouldRenderHeaders ()) { + // Render something like: + /* + ┌────────────────────┬──────────┬───────────┬──────────────┬─────────┐ + │ArithmeticComparator│chi │Healthboard│Interpretation│Labnumber│ + └────────────────────┴──────────┴───────────┴──────────────┴─────────┘ + */ + if (Style.ShowHorizontalHeaderOverline) { RenderHeaderOverline (line, bounds.Width, columnsToRender); line++; } @@ -561,76 +622,13 @@ namespace Terminal.Gui { return false; } - if (keyEvent.Key == CellActivationKey && Table != null) { - OnCellActivated (new CellActivatedEventArgs (Table, SelectedColumn, SelectedRow)); + var result = InvokeKeybindings (keyEvent); + if (result != null) { + PositionCursor (); return true; } - switch (keyEvent.Key) { - case Key.CursorLeft: - case Key.CursorLeft | Key.ShiftMask: - ChangeSelectionByOffset (-1, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.CursorRight: - case Key.CursorRight | Key.ShiftMask: - ChangeSelectionByOffset (1, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.CursorDown: - case Key.CursorDown | Key.ShiftMask: - ChangeSelectionByOffset (0, 1, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.CursorUp: - case Key.CursorUp | Key.ShiftMask: - ChangeSelectionByOffset (0, -1, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.PageUp: - case Key.PageUp | Key.ShiftMask: - ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.PageDown: - case Key.PageDown | Key.ShiftMask: - ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.Home | Key.CtrlMask: - case Key.Home | Key.CtrlMask | Key.ShiftMask: - // jump to table origin - SetSelection (0, 0, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.Home: - case Key.Home | Key.ShiftMask: - // jump to start of line - SetSelection (0, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.End | Key.CtrlMask: - case Key.End | Key.CtrlMask | Key.ShiftMask: - // jump to end of table - SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - case Key.A | Key.CtrlMask: - SelectAll (); - Update (); - break; - case Key.End: - case Key.End | Key.ShiftMask: - //jump to end of row - SetSelection (Table.Columns.Count - 1, SelectedRow, keyEvent.Key.HasFlag (Key.ShiftMask)); - Update (); - break; - default: - // Not a keystroke we care about - return false; - } - PositionCursor (); - return true; + return false; } /// @@ -671,6 +669,68 @@ namespace Terminal.Gui { public void ChangeSelectionByOffset (int offsetX, int offsetY, bool extendExistingSelection) { SetSelection (SelectedColumn + offsetX, SelectedRow + offsetY, extendExistingSelection); + Update (); + } + + /// + /// Moves the selection up by one page + /// + /// true to extend the current selection (if any) instead of replacing + public void PageUp(bool extend) + { + ChangeSelectionByOffset (0, -(Bounds.Height - GetHeaderHeightIfAny ()), extend); + Update (); + } + + /// + /// Moves the selection down by one page + /// + /// true to extend the current selection (if any) instead of replacing + public void PageDown(bool extend) + { + ChangeSelectionByOffset (0, Bounds.Height - GetHeaderHeightIfAny (), extend); + Update (); + } + + /// + /// Moves or extends the selection to the first cell in the table (0,0) + /// + /// true to extend the current selection (if any) instead of replacing + public void ChangeSelectionToStartOfTable (bool extend) + { + SetSelection (0, 0, extend); + Update (); + } + + /// + /// Moves or extends the selection to the final cell in the table + /// + /// true to extend the current selection (if any) instead of replacing + public void ChangeSelectionToEndOfTable(bool extend) + { + SetSelection (Table.Columns.Count - 1, Table.Rows.Count - 1, extend); + Update (); + } + + + /// + /// Moves or extends the selection to the last cell in the current row + /// + /// true to extend the current selection (if any) instead of replacing + public void ChangeSelectionToEndOfRow (bool extend) + { + SetSelection (Table.Columns.Count - 1, SelectedRow, extend); + Update (); + } + + /// + /// Moves or extends the selection to the first cell in the current row + /// + /// true to extend the current selection (if any) instead of replacing + public void ChangeSelectionToStartOfRow (bool extend) + { + SetSelection (0, SelectedRow, extend); + Update (); } /// diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs index b0c23de4d..2483d5f66 100644 --- a/Terminal.Gui/Views/TextField.cs +++ b/Terminal.Gui/Views/TextField.cs @@ -9,6 +9,7 @@ using System; using System.Collections.Generic; using System.Linq; using NStack; +using Rune = System.Rune; namespace Terminal.Gui { /// @@ -94,6 +95,109 @@ namespace Terminal.Gui { CanFocus = true; Used = true; WantMousePositionReports = true; + + Initialized += TextField_Initialized; + + // Things this view knows how to do + AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (); return true; }); + AddCommand (Command.LeftHomeExtend, () => { MoveHomeExtend (); return true; }); + AddCommand (Command.RightEndExtend, () => { MoveEndExtend (); return true; }); + AddCommand (Command.LeftHome, () => { MoveHome (); return true; }); + AddCommand (Command.LeftExtend, () => { MoveLeftExtend (); return true; }); + AddCommand (Command.RightExtend, () => { MoveRightExtend (); return true; }); + AddCommand (Command.WordLeftExtend, () => { MoveWordLeftExtend (); return true; }); + AddCommand (Command.WordRightExtend, () => { MoveWordRightExtend (); return true; }); + AddCommand (Command.Left, () => { MoveLeft (); return true; }); + AddCommand (Command.RightEnd, () => { MoveEnd (); return true; }); + AddCommand (Command.Right, () => { MoveRight (); return true; }); + AddCommand (Command.CutToEndLine, () => { KillToEnd (); return true; }); + AddCommand (Command.CutToStartLine, () => { KillToStart (); return true; }); + AddCommand (Command.Undo, () => { UndoChanges (); return true; }); + AddCommand (Command.Redo, () => { RedoChanges (); return true; }); + AddCommand (Command.WordLeft, () => { MoveWordLeft (); return true; }); + AddCommand (Command.WordRight, () => { MoveWordRight (); return true; }); + AddCommand (Command.KillWordForwards, () => { KillWordForwards (); return true; }); + AddCommand (Command.KillWordBackwards, () => { KillWordBackwards (); return true; }); + AddCommand (Command.ToggleOverwrite, () => { SetOverwrite (!Used); return true; }); + AddCommand (Command.EnableOverwrite, () => { SetOverwrite (true); return true; }); + AddCommand (Command.DisableOverwrite, () => { SetOverwrite (false); return true; }); + AddCommand (Command.Copy, () => { Copy (); return true; }); + AddCommand (Command.Cut, () => { Cut (); return true; }); + AddCommand (Command.Paste, () => { Paste (); return true; }); + + // 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 | Key.ShiftMask, Command.LeftHomeExtend); + AddKeyBinding (Key.Home | Key.ShiftMask | Key.CtrlMask, Command.LeftHomeExtend); + AddKeyBinding (Key.A | Key.ShiftMask | Key.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); + + AddKeyBinding (Key.Home, Command.LeftHome); + AddKeyBinding (Key.Home | Key.CtrlMask, Command.LeftHome); + AddKeyBinding (Key.A | Key.CtrlMask, Command.LeftHome); + + AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); + AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LeftExtend); + + AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); + AddKeyBinding (Key.CursorDown | Key.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); + + 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); + + AddKeyBinding (Key.CursorLeft, Command.Left); + AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); + + AddKeyBinding (Key.End, Command.RightEnd); + AddKeyBinding (Key.End | Key.CtrlMask, Command.RightEnd); + AddKeyBinding (Key.E | Key.CtrlMask, Command.RightEnd); + + AddKeyBinding (Key.CursorRight, Command.Right); + AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); + + AddKeyBinding (Key.K | Key.CtrlMask, Command.CutToEndLine); + AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine); + + AddKeyBinding (Key.Z | Key.CtrlMask, Command.Undo); + AddKeyBinding (Key.Backspace | Key.AltMask, Command.Undo); + + AddKeyBinding (Key.Y | Key.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); + + AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight); + AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.WordRight); + AddKeyBinding ((Key)((int)'F' + Key.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); + } + + + void TextField_Initialized (object sender, EventArgs e) + { + Autocomplete.HostControl = this; + Autocomplete.PopupInsideContainer = false; } /// @@ -107,6 +211,12 @@ namespace Terminal.Gui { return base.OnLeave (view); } + /// + /// Provides autocomplete context menu based on suggestions at the current cursor + /// position. Populate to enable this feature. + /// + public IAutocomplete Autocomplete { get; protected set; } = new TextFieldAutocomplete (); + /// public override Rect Frame { get => base.Frame; @@ -174,7 +284,7 @@ namespace Terminal.Gui { /// /// Sets or gets the current cursor position. /// - public int CursorPosition { + public virtual int CursorPosition { get { return point; } set { if (value < 0) { @@ -188,6 +298,11 @@ namespace Terminal.Gui { } } + /// + /// Gets the left offset position. + /// + public int ScrollOffset => first; + /// /// Sets the cursor position. /// @@ -248,6 +363,17 @@ namespace Terminal.Gui { } PositionCursor (); + + if (SelectedLength > 0) + return; + + // draw autocomplete + Autocomplete.GenerateSuggestions (); + + var renderAt = new Point ( + CursorPosition - ScrollOffset, 0); + + Autocomplete.RenderOverlay (renderAt); } Attribute GetReadOnlyColor () @@ -330,174 +456,65 @@ namespace Terminal.Gui { // Needed for the Elmish Wrapper issue https://github.com/DieselMeister/Terminal.Gui.Elmish/issues/2 oldCursorPos = point; - switch (ShortcutHelper.GetModifiersKey (kb)) { - case Key.DeleteChar: - case Key.D | Key.CtrlMask: // Delete - DeleteCharRight (); - break; - - case Key.Delete: - case Key.Backspace: - DeleteCharLeft (); - break; - - case Key.Home | Key.ShiftMask: - case Key.Home | Key.ShiftMask | Key.CtrlMask: - case Key.A | Key.ShiftMask | Key.CtrlMask: - MoveHomeExtend (); - break; - - case Key.End | Key.ShiftMask: - case Key.End | Key.ShiftMask | Key.CtrlMask: - case Key.E | Key.ShiftMask | Key.CtrlMask: - MoveEndExtend (); - break; - - // Home, C-A - case Key.Home: - case Key.Home | Key.CtrlMask: - case Key.A | Key.CtrlMask: - MoveHome (); - break; - - case Key.CursorLeft | Key.ShiftMask: - case Key.CursorUp | Key.ShiftMask: - MoveLeftExtend (); - break; - - case Key.CursorRight | Key.ShiftMask: - case Key.CursorDown | Key.ShiftMask: - MoveRightExtend (); - break; - - case Key.CursorLeft | Key.ShiftMask | Key.CtrlMask: - case Key.CursorUp | Key.ShiftMask | Key.CtrlMask: - case (Key)((int)'B' + Key.ShiftMask | Key.AltMask): - MoveWordLeftExtend (); - break; - - case Key.CursorRight | Key.ShiftMask | Key.CtrlMask: - case Key.CursorDown | Key.ShiftMask | Key.CtrlMask: - case (Key)((int)'F' + Key.ShiftMask | Key.AltMask): - MoveWordRightExtend (); - break; - - case Key.CursorLeft: - case Key.B | Key.CtrlMask: - MoveLeft (); - break; - - case Key.End: - case Key.End | Key.CtrlMask: - case Key.E | Key.CtrlMask: // End - MoveEnd (); - break; - - case Key.CursorRight: - case Key.F | Key.CtrlMask: - MoveRight (); - break; - - case Key.K | Key.CtrlMask: // kill-to-end - KillToEnd (); - break; - - case Key.K | Key.AltMask: // kill-to-start - KillToStart (); - break; - - // Undo - case Key.Z | Key.CtrlMask: - case Key.Backspace | Key.AltMask: - UndoChanges (); - break; - - //Redo - case Key.Y | Key.CtrlMask: // Control-y, yank - RedoChanges (); - break; - - case Key.CursorLeft | Key.CtrlMask: - case Key.CursorUp | Key.CtrlMask: - case (Key)((int)'B' + Key.AltMask): - MoveWordLeft (); - break; - - case Key.CursorRight | Key.CtrlMask: - case Key.CursorDown | Key.CtrlMask: - case (Key)((int)'F' + Key.AltMask): - MoveWordRight (); - break; - - case Key.DeleteChar | Key.CtrlMask: // kill-word-forwards - KillWordForwards (); - break; - - case Key.Backspace | Key.CtrlMask: // kill-word-backwards - KillWordBackwards (); - break; - - case Key.InsertChar: - InsertChar (); - break; - - case Key.C | Key.CtrlMask: - Copy (); - break; - - case Key.X | Key.CtrlMask: - Cut (); - break; - - case Key.V | Key.CtrlMask: - Paste (); - break; - - // MISSING: - // Alt-D, Alt-backspace - // Alt-Y - // Delete adding to kill buffer - - default: - // Ignore other control characters. - if (kb.Key < Key.Space || kb.Key > Key.CharMask) - return false; - - if (ReadOnly) - return true; - - if (length > 0) { - DeleteSelectedText (); - oldCursorPos = point; - } - var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key)); - if (Used) { - point++; - if (point == text.Count + 1) { - SetText (text.Concat (kbstr).ToList ()); - } else { - if (oldCursorPos > text.Count) { - oldCursorPos = text.Count; - } - SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count)))); - } - } else { - SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0)))); - point++; - } - Adjust (); + // Give autocomplete first opportunity to respond to key presses + if (SelectedLength == 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; + + // Ignore other control characters. + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; + + if (ReadOnly) + return true; + + InsertText (kb); + return true; } - void InsertChar () + void InsertText (KeyEvent kb, bool useOldCursorPos = true) { - Used = !Used; + if (length > 0) { + DeleteSelectedText (); + oldCursorPos = point; + } + if (!useOldCursorPos) { + oldCursorPos = point; + } + var kbstr = TextModel.ToRunes (ustring.Make ((uint)kb.Key)); + if (Used) { + point++; + if (point == text.Count + 1) { + SetText (text.Concat (kbstr).ToList ()); + } else { + if (oldCursorPos > text.Count) { + oldCursorPos = text.Count; + } + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (oldCursorPos, Math.Min (text.Count - oldCursorPos, text.Count)))); + } + } else { + SetText (text.GetRange (0, oldCursorPos).Concat (kbstr).Concat (text.GetRange (Math.Min (oldCursorPos + 1, text.Count), Math.Max (text.Count - oldCursorPos - 1, 0)))); + point++; + } + Adjust (); + } + + void SetOverwrite (bool overwrite) + { + Used = overwrite; SetNeedsDisplay (); } - void KillWordBackwards () + /// + /// Deletes word backwards. + /// + public virtual void KillWordBackwards () { ClearAllSelection (); int bw = WordBackward (point); @@ -508,7 +525,10 @@ namespace Terminal.Gui { Adjust (); } - void KillWordForwards () + /// + /// Deletes word forwards. + /// + public virtual void KillWordForwards () { ClearAllSelection (); int fw = WordForward (point); @@ -702,7 +722,10 @@ namespace Terminal.Gui { } } - void DeleteCharLeft () + /// + /// Deletes the left character. + /// + public virtual void DeleteCharLeft (bool useOldCursorPos = true) { if (ReadOnly) return; @@ -711,6 +734,9 @@ namespace Terminal.Gui { if (point == 0) return; + if (!useOldCursorPos) { + oldCursorPos = point; + } point--; if (oldCursorPos < text.Count) { SetText (text.GetRange (0, oldCursorPos - 1).Concat (text.GetRange (oldCursorPos, text.Count - oldCursorPos))); @@ -723,7 +749,10 @@ namespace Terminal.Gui { } } - void DeleteCharRight () + /// + /// Deletes the right character. + /// + public virtual void DeleteCharRight () { if (ReadOnly) return; @@ -868,6 +897,11 @@ namespace Terminal.Gui { return true; } + // Give autocomplete first opportunity to respond to mouse clicks + if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) { + return true; + } + if (ev.Flags == MouseFlags.Button1Pressed) { EnsureHasFocus (); PositionCursor (ev); @@ -1099,6 +1133,29 @@ namespace Terminal.Gui { return base.OnEnter (view); } + + /// + /// Inserts the given text at the current cursor position + /// exactly as if the user had just typed it + /// + /// Text to add + /// If uses the . + public void InsertText (string toAdd, bool useOldCursorPos = true) + { + foreach (var ch in toAdd) { + + Key key; + + try { + key = (Key)ch; + } catch (Exception) { + + throw new ArgumentException ($"Cannot insert character '{ch}' because it does not map to a Key"); + } + + InsertText (new KeyEvent () { Key = key }, useOldCursorPos); + } + } } /// @@ -1123,4 +1180,33 @@ namespace Terminal.Gui { NewText = newText; } } + + /// + /// Renders an overlay on another view at a given point that allows selecting + /// from a range of 'autocomplete' options. + /// An implementation on a TextField. + /// + public class TextFieldAutocomplete : Autocomplete { + + /// + protected override void DeleteTextBackwards () + { + ((TextField)HostControl).DeleteCharLeft (false); + } + + /// + protected override string GetCurrentWord () + { + var host = (TextField)HostControl; + var currentLine = host.Text.ToRuneList (); + var cursorPosition = Math.Min (host.CursorPosition, currentLine.Count); + return IdxToWord (currentLine, cursorPosition); + } + + /// + protected override void InsertText (string accepted) + { + ((TextField)HostControl).InsertText (accepted, false); + } + } } diff --git a/Terminal.Gui/Views/TextValidateField.cs b/Terminal.Gui/Views/TextValidateField.cs index 2c5314979..6d5ffedcb 100644 --- a/Terminal.Gui/Views/TextValidateField.cs +++ b/Terminal.Gui/Views/TextValidateField.cs @@ -391,6 +391,25 @@ namespace Terminal.Gui { { Height = 1; CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.LeftHome, () => { HomeKeyHandler (); return true; }); + AddCommand (Command.RightEnd, () => { EndKeyHandler (); return true; }); + AddCommand (Command.DeleteCharRight, () => { DeleteKeyHandler (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { BackspaceKeyHandler (); return true; }); + AddCommand (Command.Left, () => { CursorLeft (); return true; }); + AddCommand (Command.Right, () => { CursorRight (); return true; }); + + // Default keybindings for this view + AddKeyBinding (Key.Home, Command.LeftHome); + AddKeyBinding (Key.End, Command.RightEnd); + + AddKeyBinding (Key.Delete, Command.DeleteCharRight); + AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); + + AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); + AddKeyBinding (Key.CursorLeft, Command.Left); + AddKeyBinding (Key.CursorRight, Command.Right); } /// @@ -446,7 +465,7 @@ namespace Terminal.Gui { } } - ///inheritdoc/> + /// public override void PositionCursor () { var (left, _) = GetMargins (Frame.Width); @@ -526,6 +545,7 @@ namespace Terminal.Gui { { var current = cursorPosition; cursorPosition = provider.CursorLeft (cursorPosition); + SetNeedsDisplay (); return current != cursorPosition; } @@ -537,6 +557,7 @@ namespace Terminal.Gui { { var current = cursorPosition; cursorPosition = provider.CursorRight (cursorPosition); + SetNeedsDisplay (); return current != cursorPosition; } @@ -551,6 +572,7 @@ namespace Terminal.Gui { } cursorPosition = provider.CursorLeft (cursorPosition); provider.Delete (cursorPosition); + SetNeedsDisplay (); return true; } @@ -564,6 +586,7 @@ namespace Terminal.Gui { cursorPosition = provider.CursorLeft (cursorPosition); } provider.Delete (cursorPosition); + SetNeedsDisplay (); return true; } @@ -574,6 +597,7 @@ namespace Terminal.Gui { bool HomeKeyHandler () { cursorPosition = provider.CursorStart (); + SetNeedsDisplay (); return true; } @@ -584,6 +608,7 @@ namespace Terminal.Gui { bool EndKeyHandler () { cursorPosition = provider.CursorEnd (); + SetNeedsDisplay (); return true; } @@ -594,30 +619,21 @@ namespace Terminal.Gui { return false; } - switch (kb.Key) { - case Key.Home: HomeKeyHandler (); break; - case Key.End: EndKeyHandler (); break; - case Key.Delete: - case Key.DeleteChar: DeleteKeyHandler (); break; - case Key.Backspace: BackspaceKeyHandler (); break; - case Key.CursorLeft: CursorLeft (); break; - case Key.CursorRight: CursorRight (); break; - default: - if (kb.Key < Key.Space || kb.Key > Key.CharMask) - return false; + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; - var key = new Rune ((uint)kb.KeyValue); + if (kb.Key < Key.Space || kb.Key > Key.CharMask) + return false; - var inserted = provider.InsertAt ((char)key, cursorPosition); + var key = new Rune ((uint)kb.KeyValue); - if (inserted) { - CursorRight (); - } + var inserted = provider.InsertAt ((char)key, cursorPosition); - break; + if (inserted) { + CursorRight (); } - SetNeedsDisplay (); return true; } diff --git a/Terminal.Gui/Views/TextView.cs b/Terminal.Gui/Views/TextView.cs index 5753c4d27..436d14025 100644 --- a/Terminal.Gui/Views/TextView.cs +++ b/Terminal.Gui/Views/TextView.cs @@ -913,7 +913,7 @@ namespace Terminal.Gui { /// Provides autocomplete context menu based on suggestions at the current cursor /// position. Populate to enable this feature /// - public Autocomplete Autocomplete { get; protected set; } = new Autocomplete (); + public IAutocomplete Autocomplete { get; protected set; } = new TextViewAutocomplete (); #if false /// @@ -948,6 +948,162 @@ namespace Terminal.Gui { { CanFocus = true; Used = true; + + Initialized += TextView_Initialized; + + // Things this view knows how to do + AddCommand (Command.PageDown, () => { ProcessPageDown (); return true; }); + AddCommand (Command.PageDownExtend, () => { ProcessPageDownExtend (); return true; }); + AddCommand (Command.PageUp, () => { ProcessPageUp (); return true; }); + AddCommand (Command.PageUpExtend, () => { ProcessPageUpExtend (); return true; }); + AddCommand (Command.LineDown, () => { ProcessMoveDown (); return true; }); + AddCommand (Command.LineDownExtend, () => { ProcessMoveDownExtend (); return true; }); + AddCommand (Command.LineUp, () => { ProcessMoveUp (); return true; }); + AddCommand (Command.LineUpExtend, () => { ProcessMoveUpExtend (); return true; }); + AddCommand (Command.Right, () => ProcessMoveRight ()); + AddCommand (Command.RightExtend, () => { ProcessMoveRightExtend (); return true; }); + AddCommand (Command.Left, () => ProcessMoveLeft ()); + AddCommand (Command.LeftExtend, () => { ProcessMoveLeftExtend (); return true; }); + AddCommand (Command.DeleteCharLeft, () => { ProcessDeleteCharLeft (); return true; }); + AddCommand (Command.StartOfLine, () => { ProcessMoveStartOfLine (); return true; }); + AddCommand (Command.StartOfLineExtend, () => { ProcessMoveStartOfLineExtend (); return true; }); + AddCommand (Command.DeleteCharRight, () => { ProcessDeleteCharRight (); return true; }); + AddCommand (Command.EndOfLine, () => { ProcessMoveEndOfLine (); return true; }); + AddCommand (Command.EndOfLineExtend, () => { ProcessMoveEndOfLineExtend (); return true; }); + AddCommand (Command.CutToEndLine, () => { KillToEndOfLine (); return true; }); + AddCommand (Command.CutToStartLine, () => { KillToStartOfLine (); return true; }); + AddCommand (Command.Paste, () => { ProcessPaste (); return true; }); + AddCommand (Command.ToggleExtend, () => { ToggleSelecting (); return true; }); + AddCommand (Command.Copy, () => { ProcessCopy (); return true; }); + AddCommand (Command.Cut, () => { ProcessCut (); return true; }); + AddCommand (Command.WordLeft, () => { ProcessMoveWordBackward (); return true; }); + AddCommand (Command.WordLeftExtend, () => { ProcessMoveWordBackwardExtend (); return true; }); + AddCommand (Command.WordRight, () => { ProcessMoveWordForward (); return true; }); + AddCommand (Command.WordRightExtend, () => { ProcessMoveWordForwardExtend (); return true; }); + AddCommand (Command.KillWordForwards, () => { ProcessKillWordForward (); return true; }); + AddCommand (Command.KillWordBackwards, () => { ProcessKillWordBackward (); return true; }); + AddCommand (Command.NewLine, () => ProcessReturn ()); + AddCommand (Command.BottomEnd, () => { MoveBottomEnd (); return true; }); + AddCommand (Command.BottomEndExtend, () => { MoveBottomEndExtend (); return true; }); + AddCommand (Command.TopHome, () => { MoveTopHome (); return true; }); + AddCommand (Command.TopHomeExtend, () => { MoveTopHomeExtend (); return true; }); + AddCommand (Command.SelectAll, () => { ProcessSelectAll (); return true; }); + AddCommand (Command.ToggleOverwrite, () => { ProcessSetOverwrite (); return true; }); + AddCommand (Command.EnableOverwrite, () => { SetOverwrite (true); return true; }); + AddCommand (Command.DisableOverwrite, () => { SetOverwrite (false); return true; }); + AddCommand (Command.Tab, () => ProcessTab ()); + AddCommand (Command.BackTab, () => ProcessBackTab ()); + AddCommand (Command.NextView, () => ProcessMoveNextView ()); + AddCommand (Command.PreviousView, () => ProcessMovePreviousView ()); + + // Default keybindings for this view + AddKeyBinding (Key.PageDown, Command.PageDown); + AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown); + + AddKeyBinding (Key.PageDown | Key.ShiftMask, Command.PageDownExtend); + + AddKeyBinding (Key.PageUp, Command.PageUp); + AddKeyBinding (((int)'V' + Key.AltMask), Command.PageUp); + + AddKeyBinding (Key.PageUp | Key.ShiftMask, Command.PageUpExtend); + + AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown); + AddKeyBinding (Key.CursorDown, Command.LineDown); + + AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); + + AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp); + AddKeyBinding (Key.CursorUp, Command.LineUp); + + AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); + + AddKeyBinding (Key.F | Key.CtrlMask, Command.Right); + AddKeyBinding (Key.CursorRight, Command.Right); + + AddKeyBinding (Key.CursorRight | Key.ShiftMask, Command.RightExtend); + + AddKeyBinding (Key.B | Key.CtrlMask, Command.Left); + AddKeyBinding (Key.CursorLeft, Command.Left); + + AddKeyBinding (Key.CursorLeft | Key.ShiftMask, Command.LeftExtend); + + AddKeyBinding (Key.Delete, Command.DeleteCharLeft); + AddKeyBinding (Key.Backspace, Command.DeleteCharLeft); + + AddKeyBinding (Key.Home, Command.StartOfLine); + AddKeyBinding (Key.A | Key.CtrlMask, Command.StartOfLine); + + AddKeyBinding (Key.Home | Key.ShiftMask, Command.StartOfLineExtend); + + AddKeyBinding (Key.DeleteChar, Command.DeleteCharRight); + AddKeyBinding (Key.D | Key.CtrlMask, Command.DeleteCharRight); + + AddKeyBinding (Key.End, Command.EndOfLine); + AddKeyBinding (Key.E | Key.CtrlMask, Command.EndOfLine); + + AddKeyBinding (Key.End | Key.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 + + AddKeyBinding (Key.K | Key.AltMask, Command.CutToStartLine); // kill-to-start + AddKeyBinding (Key.Backspace | Key.CtrlMask | Key.ShiftMask, Command.CutToStartLine); // kill-to-start + + AddKeyBinding (Key.Y | Key.CtrlMask, Command.Paste); // Control-y, yank + AddKeyBinding (Key.Space | Key.CtrlMask, Command.ToggleExtend); + + AddKeyBinding (((int)'C' + Key.AltMask), Command.Copy); + AddKeyBinding (Key.C | Key.CtrlMask, Command.Copy); + + AddKeyBinding (((int)'W' + Key.AltMask), Command.Cut); + AddKeyBinding (Key.W | Key.CtrlMask, Command.Cut); + AddKeyBinding (Key.X | Key.CtrlMask, Command.Cut); + + AddKeyBinding (Key.CursorLeft | Key.CtrlMask, Command.WordLeft); + AddKeyBinding ((Key)((int)'B' + Key.AltMask), Command.WordLeft); + + AddKeyBinding (Key.CursorLeft | Key.CtrlMask | Key.ShiftMask, Command.WordLeftExtend); + + AddKeyBinding (Key.CursorRight | Key.CtrlMask, Command.WordRight); + AddKeyBinding ((Key)((int)'F' + Key.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 + + 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); + + AddKeyBinding (Key.Tab | Key.CtrlMask, Command.NextView); + AddKeyBinding (Application.AlternateForwardKey, Command.NextView); + + AddKeyBinding (Key.Tab | Key.CtrlMask | Key.ShiftMask, Command.PreviousView); + AddKeyBinding (Application.AlternateBackwardKey, Command.PreviousView); + } + + void TextView_Initialized (object sender, EventArgs e) + { + Autocomplete.HostControl = this; + + Application.Top.AlternateForwardKeyChanged += Top_AlternateForwardKeyChanged; + Application.Top.AlternateBackwardKeyChanged += Top_AlternateBackwardKeyChanged; + } + + void Top_AlternateBackwardKeyChanged (Key obj) + { + ReplaceKeyBinding (obj, Application.AlternateBackwardKey); + } + + void Top_AlternateForwardKeyChanged (Key obj) + { + ReplaceKeyBinding (obj, Application.AlternateForwardKey); } /// @@ -1119,6 +1275,9 @@ namespace Terminal.Gui { if (value == wordWrap) { return; } + if (value && !multiline) { + return; + } wordWrap = value; ResetPosition (); if (wordWrap) { @@ -1234,6 +1393,7 @@ namespace Terminal.Gui { if (!multiline) { AllowsReturn = false; AllowsTab = false; + WordWrap = false; currentColumn = 0; currentRow = 0; savedHeight = Height; @@ -1243,6 +1403,7 @@ namespace Terminal.Gui { } Height = 1; LayoutStyle = lyout; + Autocomplete.PopupInsideContainer = false; SetNeedsDisplay (); } else if (multiline && savedHeight != null) { var lyout = LayoutStyle; @@ -1251,6 +1412,7 @@ namespace Terminal.Gui { } Height = savedHeight; LayoutStyle = lyout; + Autocomplete.PopupInsideContainer = true; SetNeedsDisplay (); } } @@ -1786,14 +1948,19 @@ namespace Terminal.Gui { PositionCursor (); + if (SelectedLength > 0) + return; + // draw autocomplete - Autocomplete.GenerateSuggestions (this); + Autocomplete.GenerateSuggestions (); var renderAt = new Point ( CursorPosition.X - LeftColumn, - (CursorPosition.Y + 1) - TopRow); + Autocomplete.PopupInsideContainer + ? (CursorPosition.Y + 1) - TopRow + : 0); - Autocomplete.RenderOverlay (this, renderAt); + Autocomplete.RenderOverlay (renderAt); } /// @@ -2050,551 +2217,719 @@ namespace Terminal.Gui { return true; } - int restCount; - List rest; + // Give autocomplete first opportunity to respond to key presses + if (SelectedLength == 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); + DoNeededAction (); + + return true; + } + + bool ProcessMovePreviousView () + { + ResetColumnTrack (); + return MovePreviousView (); + } + + bool ProcessMoveNextView () + { + ResetColumnTrack (); + return MoveNextView (); + } + + void ProcessSetOverwrite () + { + ResetColumnTrack (); + SetOverwrite (!Used); + } + + void ProcessSelectAll () + { + ResetColumnTrack (); + SelectAll (); + } + + void MoveTopHomeExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveHome (); + } + + void MoveTopHome () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveHome (); + } + + void MoveBottomEndExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveEnd (); + } + + void MoveBottomEnd () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveEnd (); + } + + void ProcessKillWordBackward () + { + ResetColumnTrack (); + KillWordBackward (); + } + + void ProcessKillWordForward () + { + ResetColumnTrack (); + KillWordForward (); + } + + void ProcessMoveWordForwardExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveWordForward (); + } + + void ProcessMoveWordForward () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveWordForward (); + } + + void ProcessMoveWordBackwardExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveWordBackward (); + } + + void ProcessMoveWordBackward () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveWordBackward (); + } + + void ProcessCut () + { + ResetColumnTrack (); + Cut (); + } + + void ProcessCopy () + { + ResetColumnTrack (); + Copy (); + } + + void ToggleSelecting () + { + ResetColumnTrack (); + selecting = !selecting; + selectionStartColumn = currentColumn; + selectionStartRow = currentRow; + } + + void ProcessPaste () + { + ResetColumnTrack (); + if (isReadOnly) + return; + Paste (); + } + + void ProcessMoveEndOfLineExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveEndOfLine (); + } + + void ProcessMoveEndOfLine () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveEndOfLine (); + } + + void ProcessDeleteCharRight () + { + ResetColumnTrack (); + DeleteCharRight (); + } + + void ProcessMoveStartOfLineExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveStartOfLine (); + } + + void ProcessMoveStartOfLine () + { + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveStartOfLine (); + } + + void ProcessDeleteCharLeft () + { + ResetColumnTrack (); + DeleteCharLeft (); + } + + void ProcessMoveLeftExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveLeft (); + } + + bool ProcessMoveLeft () + { // if the user presses Left (without any control keys) and they are at the start of the text - if (kb.Key == Key.CursorLeft && currentColumn == 0 && currentRow == 0) { + if (currentColumn == 0 && currentRow == 0) { // do not respond (this lets the key press fall through to navigation system - which usually changes focus backward) return false; } + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveLeft (); + return true; + } + + void ProcessMoveRightExtend () + { + ResetAllTrack (); + StartSelecting (); + MoveRight (); + } + + bool ProcessMoveRight () + { // if the user presses Right (without any control keys) - if (kb.Key == Key.CursorRight) { + // determine where the last cursor position in the text is + var lastRow = model.Count - 1; + var lastCol = model.GetLine (lastRow).Count; - // determine where the last cursor position in the text is - var lastRow = model.Count - 1; - var lastCol = model.GetLine (lastRow).Count; - - // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward) - if (currentColumn == lastCol && currentRow == lastRow) { - return false; - } + // if they are at the very end of all the text do not respond (this lets the key press fall through to navigation system - which usually changes focus forward) + if (currentColumn == lastCol && currentRow == lastRow) { + return false; } - // Give autocomplete first opportunity to respond to key presses - if (Autocomplete.ProcessKey (this, kb)) { - return true; + ResetAllTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveRight (); + return true; + } + + void ProcessMoveUpExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveUp (); + } + + void ProcessMoveUp () + { + ResetContinuousFindTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveUp (); + } + + void ProcessMoveDownExtend () + { + ResetColumnTrack (); + StartSelecting (); + MoveDown (); + } + + void ProcessMoveDown () + { + ResetContinuousFindTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MoveDown (); + } + + void ProcessPageUpExtend () + { + ResetColumnTrack (); + StartSelecting (); + MovePageUp (); + } + + void ProcessPageUp () + { + ResetColumnTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MovePageUp (); + } + + void ProcessPageDownExtend () + { + ResetColumnTrack (); + StartSelecting (); + MovePageDown (); + } + + void ProcessPageDown () + { + ResetColumnTrack (); + if (shiftSelecting && selecting) { + StopSelecting (); + } + MovePageDown (); + } + + bool MovePreviousView () + { + if (Application.MdiTop != null) { + return SuperView?.FocusPrev () == true; } - // Handle some state here - whether the last command was a kill - // operation and the column tracking (up/down) - switch (kb.Key) { - case Key.N | Key.CtrlMask: - case Key.CursorDown: - case Key.P | Key.CtrlMask: - case Key.CursorUp: - lastWasKill = false; - continuousFind = false; - break; - case Key.K | Key.CtrlMask: - break; - case Key.F | Key.CtrlMask: - case Key.B | Key.CtrlMask: - case (Key)((int)'B' + Key.AltMask): - case Key.A | Key.CtrlMask: - case Key.E | Key.CtrlMask: - case Key.CursorRight: - case Key.CursorLeft: - case Key.CursorRight | Key.CtrlMask: - case Key.CursorLeft | Key.CtrlMask: - case Key.CursorRight | Key.ShiftMask: - case Key.CursorLeft | Key.ShiftMask: - case Key.CursorRight | Key.CtrlMask | Key.ShiftMask: - case Key.CursorLeft | Key.CtrlMask | Key.ShiftMask: - case Key.Home: - case Key.Home | Key.CtrlMask: - case Key.Home | Key.ShiftMask: - case Key.Home | Key.CtrlMask | Key.ShiftMask: - case Key.End: - case Key.End | Key.CtrlMask: - case Key.End | Key.ShiftMask: - case Key.End | Key.CtrlMask | Key.ShiftMask: - lastWasKill = false; - columnTrack = -1; - continuousFind = false; - break; - default: - lastWasKill = false; - columnTrack = -1; - break; + return false; + } + + bool MoveNextView () + { + if (Application.MdiTop != null) { + return SuperView?.FocusNext () == true; } - // Dispatch the command. - switch (kb.Key) { - case Key.PageDown: - case Key.V | Key.CtrlMask: - case Key.PageDown | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - int nPageDnShift = Frame.Height - 1; - if (currentRow >= 0 && currentRow < model.Count) { - if (columnTrack == -1) - columnTrack = currentColumn; - currentRow = (currentRow + nPageDnShift) > model.Count - ? model.Count > 0 ? model.Count - 1 : 0 - : currentRow + nPageDnShift; - if (topRow < currentRow - nPageDnShift) { - topRow = currentRow >= model.Count ? currentRow - nPageDnShift : topRow + nPageDnShift; - SetNeedsDisplay (); - } - TrackColumn (); - PositionCursor (); - } - break; + return false; + } - case Key.PageUp: - case ((int)'V' + Key.AltMask): - case Key.PageUp | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - int nPageUpShift = Frame.Height - 1; - if (currentRow > 0) { - if (columnTrack == -1) - columnTrack = currentColumn; - currentRow = currentRow - nPageUpShift < 0 ? 0 : currentRow - nPageUpShift; - if (currentRow < topRow) { - topRow = topRow - nPageUpShift < 0 ? 0 : topRow - nPageUpShift; - SetNeedsDisplay (); - } - TrackColumn (); - PositionCursor (); - } - break; + bool ProcessBackTab () + { + ResetColumnTrack (); - case Key.N | Key.CtrlMask: - case Key.CursorDown: - case Key.CursorDown | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - MoveDown (); - break; - - case Key.P | Key.CtrlMask: - case Key.CursorUp: - case Key.CursorUp | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - MoveUp (); - break; - - case Key.F | Key.CtrlMask: - case Key.CursorRight: - case Key.CursorRight | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } + if (!AllowsTab) { + return false; + } + if (currentColumn > 0) { var currentLine = GetCurrentLine (); - if (currentColumn < currentLine.Count) { - currentColumn++; - } else { - if (currentRow + 1 < model.Count) { - currentRow++; - currentColumn = 0; - if (currentRow >= topRow + Frame.Height) { - topRow++; - SetNeedsDisplay (); - } - } - } - Adjust (); - return true; - - case Key.B | Key.CtrlMask: - case Key.CursorLeft: - case Key.CursorLeft | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - if (currentColumn > 0) { + if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') { + currentLine.RemoveAt (currentColumn - 1); currentColumn--; - } else { - if (currentRow > 0) { - currentRow--; - if (currentRow < topRow) { - topRow--; - SetNeedsDisplay (); - } - currentLine = GetCurrentLine (); - currentColumn = currentLine.Count; - } } - Adjust (); - break; + } + DoNeededAction (); + return true; + } - case Key.Delete: - case Key.Backspace: - if (isReadOnly) - break; - if (selecting) { - ClearSelectedRegion (); - return true; - } - if (DeleteTextBackwards ()) { - return true; - } - break; + bool ProcessTab () + { + ResetColumnTrack (); - // Home, C-A - case Key.Home: - case Key.Home | Key.ShiftMask: - case Key.A | Key.CtrlMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - currentColumn = 0; + if (!AllowsTab) { + return false; + } + InsertText (new KeyEvent ((Key)'\t', null)); + DoNeededAction (); + return true; + } + + void SetOverwrite (bool overwrite) + { + Used = overwrite; + SetNeedsDisplay (); + DoNeededAction (); + } + + bool ProcessReturn () + { + ResetColumnTrack (); + + if (!AllowsReturn) { + return false; + } + if (isReadOnly) + return true; + var currentLine = GetCurrentLine (); + var restCount = currentLine.Count - currentColumn; + var rest = currentLine.GetRange (currentColumn, restCount); + currentLine.RemoveRange (currentColumn, restCount); + model.AddLine (currentRow + 1, rest); + if (wordWrap) { + wrapManager.AddLine (currentRow, currentColumn); + wrapNeeded = true; + } + currentRow++; + bool fullNeedsDisplay = false; + if (currentRow >= topRow + Frame.Height) { + topRow++; + fullNeedsDisplay = true; + } + currentColumn = 0; + if (!wordWrap && currentColumn < leftColumn) { + fullNeedsDisplay = true; leftColumn = 0; - Adjust (); - break; - case Key.DeleteChar: - case Key.D | Key.CtrlMask: // Delete - if (isReadOnly) - break; - if (selecting) { - ClearSelectedRegion (); - return true; - } - if (DeleteTextForwards ()) { - return true; - } - break; + } - case Key.End: - case Key.End | Key.ShiftMask: - case Key.E | Key.CtrlMask: // End - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - currentLine = GetCurrentLine (); - currentColumn = currentLine.Count; - Adjust (); - return true; + if (fullNeedsDisplay) + SetNeedsDisplay (); + else + SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height)); - case Key.K | Key.CtrlMask: // kill-to-end - case Key.DeleteChar | Key.CtrlMask | Key.ShiftMask: - if (isReadOnly) - break; - currentLine = GetCurrentLine (); - var setLastWasKill = true; - if (currentLine.Count > 0 && currentColumn == currentLine.Count) { - DeleteTextForwards (); - return true; - } - if (currentLine.Count == 0) { - if (currentRow < model.Count - 1) { - model.RemoveLine (currentRow); - } - if (model.Count > 0 || lastWasKill) { - var val = ustring.Make ((Rune)'\n'); - if (lastWasKill) { - AppendClipboard (val); - } else { - SetClipboard (val); - } - } - if (model.Count == 0) { - // Prevents from adding line feeds if there is no more lines. - setLastWasKill = false; - } - } else { - restCount = currentLine.Count - currentColumn; - rest = currentLine.GetRange (currentColumn, restCount); - var val = ustring.Empty; - if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { - val = ustring.Make ((Rune)'\n'); - } - val += StringFromRunes (rest); - if (lastWasKill) { - AppendClipboard (val); - } else { - SetClipboard (val); - } - currentLine.RemoveRange (currentColumn, restCount); - } - SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); - lastWasKill = setLastWasKill; - break; + DoNeededAction (); + return true; + } - case Key.K | Key.AltMask: // kill-to-start - if (isReadOnly) - break; - currentLine = GetCurrentLine (); - setLastWasKill = true; - if (currentLine.Count > 0 && currentColumn == 0) { - DeleteTextBackwards (); - return true; - } - if (currentLine.Count == 0) { - if (currentRow > 0) { - model.RemoveLine (currentRow); - currentRow--; - currentLine = model.GetLine (currentRow); - currentColumn = currentLine.Count; - } - } else { - restCount = currentColumn; - rest = currentLine.GetRange (0, restCount); - var val = ustring.Empty; - if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { - val = ustring.Make ((Rune)'\n'); - } - val += StringFromRunes (rest); - if (lastWasKill) { - AppendClipboard (val); - } else { - SetClipboard (val); - } - currentLine.RemoveRange (0, restCount); - currentColumn = 0; - } - SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); - lastWasKill = setLastWasKill; - break; - - case Key.Y | Key.CtrlMask: // Control-y, yank - if (isReadOnly) - break; - Paste (); - return true; - - case Key.Space | Key.CtrlMask: - selecting = !selecting; - selectionStartColumn = currentColumn; - selectionStartRow = currentRow; - break; - - case ((int)'C' + Key.AltMask): - case Key.C | Key.CtrlMask: - Copy (); - return true; - - case ((int)'W' + Key.AltMask): - case Key.W | Key.CtrlMask: - case Key.X | Key.CtrlMask: - Cut (); - return true; - - case Key.CtrlMask | Key.CursorLeft: - case Key.CtrlMask | Key.CursorLeft | Key.ShiftMask: - case (Key)((int)'B' + Key.AltMask): - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - var newPos = WordBackward (currentColumn, currentRow); - if (newPos.HasValue) { - currentColumn = newPos.Value.col; - currentRow = newPos.Value.row; - } - Adjust (); - - break; - - case Key.CtrlMask | Key.CursorRight: - case Key.CtrlMask | Key.CursorRight | Key.ShiftMask: - case (Key)((int)'F' + Key.AltMask): - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - newPos = WordForward (currentColumn, currentRow); - if (newPos.HasValue) { - currentColumn = newPos.Value.col; - currentRow = newPos.Value.row; - } - Adjust (); - break; - - case Key.DeleteChar | Key.CtrlMask: // kill-word-forwards - if (isReadOnly) - break; - currentLine = GetCurrentLine (); - if (currentLine.Count == 0 || currentColumn == currentLine.Count) { - DeleteTextForwards (); - return true; - } - newPos = WordForward (currentColumn, currentRow); - restCount = 0; - if (newPos.HasValue && currentRow == newPos.Value.row) { - restCount = newPos.Value.col - currentColumn; - currentLine.RemoveRange (currentColumn, restCount); - } else if (newPos.HasValue) { - restCount = currentLine.Count - currentColumn; - currentLine.RemoveRange (currentColumn, restCount); + void KillWordBackward () + { + if (isReadOnly) + return; + var currentLine = GetCurrentLine (); + if (currentColumn == 0) { + DeleteTextBackwards (); + return; + } + var newPos = WordBackward (currentColumn, currentRow); + if (newPos.HasValue && currentRow == newPos.Value.row) { + var restCount = currentColumn - newPos.Value.col; + currentLine.RemoveRange (newPos.Value.col, restCount); + if (wordWrap && wrapManager.RemoveRange (currentRow, newPos.Value.col, restCount)) { + wrapNeeded = true; } + currentColumn = newPos.Value.col; + } else if (newPos.HasValue) { + var restCount = currentLine.Count - currentColumn; + currentLine.RemoveRange (currentColumn, restCount); if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) { wrapNeeded = true; } - if (wrapNeeded) { - SetNeedsDisplay (); - } else { - SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); - } - break; - - case Key.Backspace | Key.CtrlMask: // kill-word-backwards - if (isReadOnly) - break; - currentLine = GetCurrentLine (); - if (currentColumn == 0) { - DeleteTextBackwards (); - return true; - } - newPos = WordBackward (currentColumn, currentRow); - if (newPos.HasValue && currentRow == newPos.Value.row) { - restCount = currentColumn - newPos.Value.col; - currentLine.RemoveRange (newPos.Value.col, restCount); - if (wordWrap && wrapManager.RemoveRange (currentRow, newPos.Value.col, restCount)) { - wrapNeeded = true; - } - currentColumn = newPos.Value.col; - } else if (newPos.HasValue) { - restCount = currentLine.Count - currentColumn; - currentLine.RemoveRange (currentColumn, restCount); - if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) { - wrapNeeded = true; - } - currentColumn = newPos.Value.col; - currentRow = newPos.Value.row; - } - if (wrapNeeded) { - SetNeedsDisplay (); - } else { - SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); - } - break; - - case Key.Enter: - if (!AllowsReturn) { - return false; - } - if (isReadOnly) - break; - currentLine = GetCurrentLine (); - restCount = currentLine.Count - currentColumn; - rest = currentLine.GetRange (currentColumn, restCount); - currentLine.RemoveRange (currentColumn, restCount); - model.AddLine (currentRow + 1, rest); - if (wordWrap) { - wrapManager.AddLine (currentRow, currentColumn); - wrapNeeded = true; - } - currentRow++; - bool fullNeedsDisplay = false; - if (currentRow >= topRow + Frame.Height) { - topRow++; - fullNeedsDisplay = true; - } - currentColumn = 0; - if (!wordWrap && currentColumn < leftColumn) { - fullNeedsDisplay = true; - leftColumn = 0; - } - - if (fullNeedsDisplay) - SetNeedsDisplay (); - else - SetNeedsDisplay (new Rect (0, currentRow - topRow, 2, Frame.Height)); - break; - - case Key.CtrlMask | Key.End: - case Key.CtrlMask | Key.End | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - MoveEnd (); - break; - - case Key.CtrlMask | Key.Home: - case Key.CtrlMask | Key.Home | Key.ShiftMask: - if (kb.Key.HasFlag (Key.ShiftMask)) { - StartSelecting (); - } else if (shiftSelecting && selecting) { - StopSelecting (); - } - MoveHome (); - break; - - case Key.T | Key.CtrlMask: - SelectAll (); - break; - - case Key.InsertChar: - Used = !Used; + currentColumn = newPos.Value.col; + currentRow = newPos.Value.row; + } + if (wrapNeeded) { SetNeedsDisplay (); - break; - - case Key _ when ShortcutHelper.GetModifiersKey (kb) == Key.Tab: - if (!AllowsTab) { - return false; - } - InsertText (new KeyEvent ((Key)'\t', null)); - break; - - case Key _ when (ShortcutHelper.GetModifiersKey (kb) == (Key.BackTab | Key.ShiftMask)): - if (!AllowsTab) { - return false; - } - if (currentColumn > 0) { - currentLine = GetCurrentLine (); - if (currentLine.Count > 0 && currentLine [currentColumn - 1] == '\t') { - currentLine.RemoveAt (currentColumn - 1); - currentColumn--; - } - } - break; - - case Key _ when ShortcutHelper.GetModifiersKey (kb) == (Key.Tab | Key.CtrlMask): - case Key _ when ShortcutHelper.GetModifiersKey (kb) == Application.AlternateForwardKey: - if (Application.MdiTop != null) { - return SuperView?.FocusNext () == true; - } - - return false; - - case Key _ when ShortcutHelper.GetModifiersKey (kb) == (Key.Tab | Key.CtrlMask | Key.ShiftMask): - case Key _ when ShortcutHelper.GetModifiersKey (kb) == Application.AlternateBackwardKey: - if (Application.MdiTop != null) { - return SuperView?.FocusPrev () == true; - } - - return false; - - default: - // Ignore control characters and other special keys - if (kb.Key < Key.Space || kb.Key > Key.CharMask) - return false; - - InsertText (kb); - break; + } else { + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); } DoNeededAction (); + } - return true; + void KillWordForward () + { + if (isReadOnly) + return; + var currentLine = GetCurrentLine (); + if (currentLine.Count == 0 || currentColumn == currentLine.Count) { + DeleteTextForwards (); + return; + } + var newPos = WordForward (currentColumn, currentRow); + var restCount = 0; + if (newPos.HasValue && currentRow == newPos.Value.row) { + restCount = newPos.Value.col - currentColumn; + currentLine.RemoveRange (currentColumn, restCount); + } else if (newPos.HasValue) { + restCount = currentLine.Count - currentColumn; + currentLine.RemoveRange (currentColumn, restCount); + } + if (wordWrap && wrapManager.RemoveRange (currentRow, currentColumn, restCount)) { + wrapNeeded = true; + } + if (wrapNeeded) { + SetNeedsDisplay (); + } else { + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); + } + DoNeededAction (); + } + + void MoveWordForward () + { + var newPos = WordForward (currentColumn, currentRow); + if (newPos.HasValue) { + currentColumn = newPos.Value.col; + currentRow = newPos.Value.row; + } + Adjust (); + DoNeededAction (); + } + + void MoveWordBackward () + { + var newPos = WordBackward (currentColumn, currentRow); + if (newPos.HasValue) { + currentColumn = newPos.Value.col; + currentRow = newPos.Value.row; + } + Adjust (); + DoNeededAction (); + } + + void KillToStartOfLine () + { + ResetColumnTrack (); + if (isReadOnly) + return; + var currentLine = GetCurrentLine (); + var setLastWasKill = true; + if (currentLine.Count > 0 && currentColumn == 0) { + DeleteTextBackwards (); + return; + } + if (currentLine.Count == 0) { + if (currentRow > 0) { + model.RemoveLine (currentRow); + currentRow--; + currentLine = model.GetLine (currentRow); + currentColumn = currentLine.Count; + } + } else { + var restCount = currentColumn; + var rest = currentLine.GetRange (0, restCount); + var val = ustring.Empty; + if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { + val = ustring.Make ((Rune)'\n'); + } + val += StringFromRunes (rest); + if (lastWasKill) { + AppendClipboard (val); + } else { + SetClipboard (val); + } + currentLine.RemoveRange (0, restCount); + currentColumn = 0; + } + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); + lastWasKill = setLastWasKill; + DoNeededAction (); + } + + void KillToEndOfLine () + { + ResetColumnTrack (); + if (isReadOnly) + return; + var currentLine = GetCurrentLine (); + var setLastWasKill = true; + if (currentLine.Count > 0 && currentColumn == currentLine.Count) { + DeleteTextForwards (); + return; + } + if (currentLine.Count == 0) { + if (currentRow < model.Count - 1) { + model.RemoveLine (currentRow); + } + if (model.Count > 0 || lastWasKill) { + var val = ustring.Make ((Rune)'\n'); + if (lastWasKill) { + AppendClipboard (val); + } else { + SetClipboard (val); + } + } + if (model.Count == 0) { + // Prevents from adding line feeds if there is no more lines. + setLastWasKill = false; + } + } else { + var restCount = currentLine.Count - currentColumn; + var rest = currentLine.GetRange (currentColumn, restCount); + var val = ustring.Empty; + if (currentColumn == 0 && lastWasKill && currentLine.Count > 0) { + val = ustring.Make ((Rune)'\n'); + } + val += StringFromRunes (rest); + if (lastWasKill) { + AppendClipboard (val); + } else { + SetClipboard (val); + } + currentLine.RemoveRange (currentColumn, restCount); + } + SetNeedsDisplay (new Rect (0, currentRow - topRow, Frame.Width, Frame.Height)); + lastWasKill = setLastWasKill; + DoNeededAction (); + } + + void MoveEndOfLine () + { + var currentLine = GetCurrentLine (); + currentColumn = currentLine.Count; + Adjust (); + DoNeededAction (); + } + + void MoveStartOfLine () + { + currentColumn = 0; + leftColumn = 0; + Adjust (); + DoNeededAction (); + } + + void DeleteCharRight () + { + if (isReadOnly) + return; + if (selecting) { + ClearSelectedRegion (); + return; + } + if (DeleteTextForwards ()) { + return; + } + DoNeededAction (); + } + + void DeleteCharLeft () + { + if (isReadOnly) + return; + if (selecting) { + ClearSelectedRegion (); + return; + } + if (DeleteTextBackwards ()) { + return; + } + DoNeededAction (); + } + + void MoveLeft () + { + if (currentColumn > 0) { + currentColumn--; + } else { + if (currentRow > 0) { + currentRow--; + if (currentRow < topRow) { + topRow--; + SetNeedsDisplay (); + } + var currentLine = GetCurrentLine (); + currentColumn = currentLine.Count; + } + } + Adjust (); + DoNeededAction (); + } + + void MoveRight () + { + var currentLine = GetCurrentLine (); + if (currentColumn < currentLine.Count) { + currentColumn++; + } else { + if (currentRow + 1 < model.Count) { + currentRow++; + currentColumn = 0; + if (currentRow >= topRow + Frame.Height) { + topRow++; + SetNeedsDisplay (); + } + } + } + Adjust (); + DoNeededAction (); + } + + void MovePageUp () + { + int nPageUpShift = Frame.Height - 1; + if (currentRow > 0) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow = currentRow - nPageUpShift < 0 ? 0 : currentRow - nPageUpShift; + if (currentRow < topRow) { + topRow = topRow - nPageUpShift < 0 ? 0 : topRow - nPageUpShift; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + DoNeededAction (); + } + + void MovePageDown () + { + int nPageDnShift = Frame.Height - 1; + if (currentRow >= 0 && currentRow < model.Count) { + if (columnTrack == -1) + columnTrack = currentColumn; + currentRow = (currentRow + nPageDnShift) > model.Count + ? model.Count > 0 ? model.Count - 1 : 0 + : currentRow + nPageDnShift; + if (topRow < currentRow - nPageDnShift) { + topRow = currentRow >= model.Count ? currentRow - nPageDnShift : topRow + nPageDnShift; + SetNeedsDisplay (); + } + TrackColumn (); + PositionCursor (); + } + DoNeededAction (); + } + + void ResetContinuousFindTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + lastWasKill = false; + continuousFind = false; + } + + void ResetColumnTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + lastWasKill = false; + columnTrack = -1; + } + + void ResetAllTrack () + { + // Handle some state here - whether the last command was a kill + // operation and the column tracking (up/down) + lastWasKill = false; + columnTrack = -1; + continuousFind = false; } bool InsertText (KeyEvent kb) @@ -2820,6 +3155,7 @@ namespace Terminal.Gui { TrackColumn (); PositionCursor (); } + DoNeededAction (); } void MoveDown () @@ -2838,6 +3174,7 @@ namespace Terminal.Gui { } else if (currentRow > Frame.Height) { Adjust (); } + DoNeededAction (); } IEnumerable<(int col, int row, Rune rune)> ForwardIterator (int col, int row) @@ -3094,6 +3431,11 @@ namespace Terminal.Gui { continuousFind = false; + // Give autocomplete first opportunity to respond to mouse clicks + if (SelectedLength == 0 && Autocomplete.MouseEvent (ev, true)) { + return true; + } + if (ev.Flags == MouseFlags.Button1Clicked) { if (shiftSelecting) { shiftSelecting = false; @@ -3250,4 +3592,32 @@ namespace Terminal.Gui { } } + /// + /// Renders an overlay on another view at a given point that allows selecting + /// from a range of 'autocomplete' options. + /// An implementation on a TextView. + /// + public class TextViewAutocomplete : Autocomplete { + + /// + protected override string GetCurrentWord () + { + var host = (TextView)HostControl; + var currentLine = host.GetCurrentLine (); + var cursorPosition = Math.Min (host.CurrentColumn, currentLine.Count); + return IdxToWord (currentLine, cursorPosition); + } + + /// + protected override void DeleteTextBackwards () + { + ((TextView)HostControl).DeleteTextBackwards (); + } + + /// + protected override void InsertText (string accepted) + { + ((TextView)HostControl).InsertText (accepted); + } + } } diff --git a/Terminal.Gui/Views/TimeField.cs b/Terminal.Gui/Views/TimeField.cs index bf22d9fec..1675a295a 100644 --- a/Terminal.Gui/Views/TimeField.cs +++ b/Terminal.Gui/Views/TimeField.cs @@ -26,8 +26,8 @@ namespace Terminal.Gui { string longFormat; string shortFormat; - int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } } - string Format { get { return isShort ? shortFormat : longFormat; } } + int fieldLen => isShort ? shortFieldLen : longFieldLen; + string format => isShort ? shortFormat : longFormat; /// /// TimeChanged event, raised when the Date has changed. @@ -49,8 +49,7 @@ namespace Terminal.Gui { /// If true, the seconds are hidden. Sets the property. public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "") { - this.isShort = isShort; - Initialize (time); + Initialize (time, isShort); } /// @@ -59,8 +58,7 @@ namespace Terminal.Gui { /// Initial time public TimeField (TimeSpan time) : base (string.Empty) { - this.isShort = true; - Width = FieldLen + 2; + Width = fieldLen + 2; Initialize (time); } @@ -69,21 +67,49 @@ namespace Terminal.Gui { /// public TimeField () : this (time: TimeSpan.MinValue) { } - void Initialize (TimeSpan time) + void Initialize (TimeSpan time, bool isShort = false) { CultureInfo cultureInfo = CultureInfo.CurrentCulture; sepChar = cultureInfo.DateTimeFormat.TimeSeparator; longFormat = $" hh\\{sepChar}mm\\{sepChar}ss"; shortFormat = $" hh\\{sepChar}mm"; - CursorPosition = 1; + this.isShort = isShort; Time = time; + CursorPosition = 1; TextChanged += TextField_TextChanged; + + // 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 TextField_TextChanged (ustring e) { try { - if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) + if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) Text = e; } catch (Exception) { Text = e; @@ -105,8 +131,8 @@ namespace Terminal.Gui { var oldTime = time; time = value; - this.Text = " " + value.ToString (Format.Trim ()); - var args = new DateTimeEventArgs (oldTime, value, Format); + this.Text = " " + value.ToString (format.Trim ()); + var args = new DateTimeEventArgs (oldTime, value, format); if (oldTime != value) { OnTimeChanged (args); } @@ -133,12 +159,20 @@ namespace Terminal.Gui { } } + /// + public override int CursorPosition { + get => base.CursorPosition; + set { + base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); + } + } + bool SetText (Rune key) { var text = TextModel.ToRunes (Text); var newText = text.GetRange (0, CursorPosition); newText.Add (key); - if (CursorPosition < FieldLen) + if (CursorPosition < fieldLen) newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); return SetText (ustring.Make (newText)); } @@ -183,7 +217,7 @@ namespace Terminal.Gui { } string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}"; - if (!TimeSpan.TryParseExact (t.Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) || + if (!TimeSpan.TryParseExact (t.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) || !isValidTime) return false; Time = result; @@ -192,7 +226,7 @@ namespace Terminal.Gui { void IncCursorPosition () { - if (CursorPosition == FieldLen) + if (CursorPosition == fieldLen) return; if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) CursorPosition++; @@ -215,60 +249,69 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent kb) { - switch (kb.Key) { - case Key.DeleteChar: - case Key.D | Key.CtrlMask: - if (ReadOnly) - return true; + var result = InvokeKeybindings (kb); + if (result != null) + return (bool)result; - SetText ('0'); - break; + // Ignore non-numeric characters. + if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9)) + return false; - case Key.Delete: - case Key.Backspace: - if (ReadOnly) - return true; - - SetText ('0'); - DecCursorPosition (); - break; - - // Home, C-A - case Key.Home: - case Key.A | Key.CtrlMask: - CursorPosition = 1; - break; - - case Key.CursorLeft: - case Key.B | Key.CtrlMask: - DecCursorPosition (); - break; - - case Key.End: - case Key.E | Key.CtrlMask: // End - CursorPosition = FieldLen; - break; - - case Key.CursorRight: - case Key.F | Key.CtrlMask: - IncCursorPosition (); - break; - - default: - // Ignore non-numeric characters. - if (kb.Key < (Key)((int)Key.D0) || kb.Key > (Key)((int)Key.D9)) - return false; - - if (ReadOnly) - return true; - - if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) - IncCursorPosition (); + if (ReadOnly) return true; - } + + if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ())) + IncCursorPosition (); + return true; } + bool MoveRight () + { + IncCursorPosition (); + return true; + } + + 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 ('0'); + DecCursorPosition (); + return; + } + + /// + public override void DeleteCharRight () + { + if (ReadOnly) + return; + + SetText ('0'); + return; + } + /// public override bool MouseEvent (MouseEvent ev) { @@ -278,8 +321,8 @@ namespace Terminal.Gui { SetFocus (); var point = ev.X; - if (point > FieldLen) - point = FieldLen; + if (point > fieldLen) + point = fieldLen; if (point < 1) point = 1; CursorPosition = point; diff --git a/Terminal.Gui/Views/TreeView.cs b/Terminal.Gui/Views/TreeView.cs index bcea9739a..b8908bd8c 100644 --- a/Terminal.Gui/Views/TreeView.cs +++ b/Terminal.Gui/Views/TreeView.cs @@ -2,11 +2,11 @@ // by phillip.piper@gmail.com). Phillip has explicitly granted permission for his design // and code to be used in this library under the MIT license. +using NStack; using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using NStack; using Terminal.Gui.Trees; namespace Terminal.Gui { @@ -122,7 +122,15 @@ namespace Terminal.Gui { /// Key which when pressed triggers . /// Defaults to Enter /// - public Key ObjectActivationKey { get; set; } = Key.Enter; + public Key ObjectActivationKey { + get => objectActivationKey; + set { + if (objectActivationKey != value) { + ReplaceKeyBinding (ObjectActivationKey, value); + objectActivationKey = value; + } + } + } /// /// Mouse event to trigger . @@ -148,6 +156,7 @@ namespace Terminal.Gui { /// (nodes added but no tree builder set) /// public static ustring NoBuilderError = "ERROR: TreeBuilder Not Set"; + private Key objectActivationKey = Key.Enter; /// /// Called when the changes @@ -227,6 +236,54 @@ namespace Terminal.Gui { public TreeView () : base () { CanFocus = true; + + // Things this view knows how to do + AddCommand (Command.PageUp, () => { MovePageUp (false); return true; }); + AddCommand (Command.PageDown, () => { MovePageDown (false); return true; }); + AddCommand (Command.PageUpExtend, () => { MovePageUp (true); return true; }); + AddCommand (Command.PageDownExtend, () => { MovePageDown (true); return true; }); + AddCommand (Command.Expand, () => { Expand (); return true; }); + AddCommand (Command.ExpandAll, () => { ExpandAll (SelectedObject); return true; }); + AddCommand (Command.Collapse, () => { CursorLeft (false); return true; }); + AddCommand (Command.CollapseAll, () => { CursorLeft (true); return true; }); + AddCommand (Command.LineUp, () => { AdjustSelection (-1, false); return true; }); + AddCommand (Command.LineUpExtend, () => { AdjustSelection (-1, true); return true; }); + AddCommand (Command.LineUpToFirstBranch, () => { AdjustSelectionToBranchStart (); return true; }); + + AddCommand (Command.LineDown, () => { AdjustSelection (1, false); return true; }); + AddCommand (Command.LineDownExtend, () => { AdjustSelection (1, true); return true; }); + AddCommand (Command.LineDownToLastBranch, () => { AdjustSelectionToBranchEnd (); return true; }); + + AddCommand (Command.TopHome, () => { GoToFirst (); return true; }); + AddCommand (Command.BottomEnd, () => { GoToEnd (); return true; }); + AddCommand (Command.SelectAll, () => { SelectAll (); return true; }); + + AddCommand (Command.ScrollUp, () => { ScrollUp (); return true; }); + AddCommand (Command.ScrollDown, () => { ScrollDown (); return true; }); + 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); + + AddKeyBinding (Key.CursorUp, Command.LineUp); + AddKeyBinding (Key.CursorUp | Key.ShiftMask, Command.LineUpExtend); + AddKeyBinding (Key.CursorUp | Key.CtrlMask, Command.LineUpToFirstBranch); + + AddKeyBinding (Key.CursorDown, Command.LineDown); + AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDownExtend); + AddKeyBinding (Key.CursorDown | Key.CtrlMask, Command.LineDownToLastBranch); + + AddKeyBinding (Key.Home, Command.TopHome); + AddKeyBinding (Key.End, Command.BottomEnd); + AddKeyBinding (Key.A | Key.CtrlMask, Command.SelectAll); + AddKeyBinding (ObjectActivationKey, Command.Accept); } /// @@ -504,87 +561,98 @@ namespace Terminal.Gui { /// public override bool ProcessKey (KeyEvent keyEvent) { - if (keyEvent.Key == ObjectActivationKey) { - var o = SelectedObject; - - if (o != null) { - OnObjectActivated (new ObjectActivatedEventArgs (this, o)); - - PositionCursor (); - return true; - } - } - - if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { - - var character = (char)keyEvent.KeyValue; - - // if it is a single character pressed without any control keys - if (char.IsLetterOrDigit (character) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { - // search for next branch that begins with that letter - var characterAsStr = character.ToString (); - AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, StringComparison.CurrentCultureIgnoreCase)); - - PositionCursor (); - return true; - } - - } - - - switch (keyEvent.Key) { - - case Key.CursorRight: - Expand (SelectedObject); - break; - case Key.CursorRight | Key.CtrlMask: - ExpandAll (SelectedObject); - break; - case Key.CursorLeft: - case Key.CursorLeft | Key.CtrlMask: - CursorLeft (keyEvent.Key.HasFlag (Key.CtrlMask)); - break; - - case Key.CursorUp: - case Key.CursorUp | Key.ShiftMask: - AdjustSelection (-1, keyEvent.Key.HasFlag (Key.ShiftMask)); - break; - case Key.CursorDown: - case Key.CursorDown | Key.ShiftMask: - AdjustSelection (1, keyEvent.Key.HasFlag (Key.ShiftMask)); - break; - case Key.CursorUp | Key.CtrlMask: - AdjustSelectionToBranchStart (); - break; - case Key.CursorDown | Key.CtrlMask: - AdjustSelectionToBranchEnd (); - break; - case Key.PageUp: - case Key.PageUp | Key.ShiftMask: - AdjustSelection (-Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask)); - break; - - case Key.PageDown: - case Key.PageDown | Key.ShiftMask: - AdjustSelection (Bounds.Height, keyEvent.Key.HasFlag (Key.ShiftMask)); - break; - case Key.A | Key.CtrlMask: - SelectAll (); - break; - case Key.Home: - GoToFirst (); - break; - case Key.End: - GoToEnd (); - break; - - default: - // we don't care about this keystroke + if (!Enabled) { return false; } + // if it is a single character pressed without any control keys + if (keyEvent.KeyValue > 0 && keyEvent.KeyValue < 0xFFFF) { + + if (char.IsLetterOrDigit ((char)keyEvent.KeyValue) && AllowLetterBasedNavigation && !keyEvent.IsShift && !keyEvent.IsAlt && !keyEvent.IsCtrl) { + AdjustSelectionToNextItemBeginningWith ((char)keyEvent.KeyValue); + return true; + } + } + + try { + var result = InvokeKeybindings (keyEvent); + if (result != null) + return (bool)result; + } finally { + + PositionCursor (); + } + + return base.ProcessKey (keyEvent); + } + + + /// + /// Triggers the event with the . + /// + /// This method also ensures that the selected object is visible + /// + public void ActivateSelectedObjectIfAny () + { + var o = SelectedObject; + + if (o != null) { + OnObjectActivated (new ObjectActivatedEventArgs (this, o)); + PositionCursor (); + } + } + + /// + /// Moves the to the next item that begins with + /// This method will loop back to the start of the tree if reaching the end without finding a match + /// + /// The first character of the next item you want selected + /// Case sensitivity of the search + public void AdjustSelectionToNextItemBeginningWith (char character, StringComparison caseSensitivity = StringComparison.CurrentCultureIgnoreCase) + { + // search for next branch that begins with that letter + var characterAsStr = character.ToString (); + AdjustSelectionToNext (b => AspectGetter (b.Model).StartsWith (characterAsStr, caseSensitivity)); + PositionCursor (); - return true; + } + + /// + /// Moves the selection up by the height of the control (1 page). + /// + /// True if the navigation should add the covered nodes to the selected current selection + /// + public void MovePageUp (bool expandSelection = false) + { + AdjustSelection (-Bounds.Height, expandSelection); + } + + /// + /// Moves the selection down by the height of the control (1 page). + /// + /// True if the navigation should add the covered nodes to the selected current selection + /// + public void MovePageDown (bool expandSelection = false) + { + AdjustSelection (Bounds.Height, expandSelection); + } + + /// + /// Scrolls the view area down a single line without changing the current selection + /// + public void ScrollDown () + { + ScrollOffsetVertical++; + SetNeedsDisplay (); + } + + /// + /// Scrolls the view area up a single line without changing the current selection + /// + public void ScrollUp () + { + ScrollOffsetVertical--; + SetNeedsDisplay (); } /// @@ -618,13 +686,11 @@ namespace Terminal.Gui { if (me.Flags == MouseFlags.WheeledDown) { - ScrollOffsetVertical++; - SetNeedsDisplay (); + ScrollDown (); return true; } else if (me.Flags == MouseFlags.WheeledUp) { - ScrollOffsetVertical--; - SetNeedsDisplay (); + ScrollUp (); return true; } @@ -736,7 +802,7 @@ namespace Terminal.Gui { if (CanFocus && HasFocus && Visible && SelectedObject != null) { var map = BuildLineMap (); - var idx = map.IndexOf(b => b.Model.Equals (SelectedObject)); + var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); // if currently selected line is visible if (idx - ScrollOffsetVertical >= 0 && idx - ScrollOffsetVertical < Bounds.Height) { @@ -839,7 +905,7 @@ namespace Terminal.Gui { } else { var map = BuildLineMap (); - var idx = map.IndexOf(b => b.Model.Equals (SelectedObject)); + var idx = map.IndexOf (b => b.Model.Equals (SelectedObject)); if (idx == -1) { @@ -848,7 +914,7 @@ namespace Terminal.Gui { } else { var newIdx = Math.Min (Math.Max (0, idx + offset), map.Count - 1); - var newBranch = map.ElementAt(newIdx); + var newBranch = map.ElementAt (newIdx); // If it is a multi selection if (expandSelection && MultiSelect) { @@ -858,7 +924,7 @@ namespace Terminal.Gui { multiSelectedRegions.Push (new TreeSelection (head.Origin, newIdx, map)); } else { // or start a new multi selection region - multiSelectedRegions.Push (new TreeSelection (map.ElementAt(idx), newIdx, map)); + multiSelectedRegions.Push (new TreeSelection (map.ElementAt (idx), newIdx, map)); } } @@ -884,13 +950,13 @@ namespace Terminal.Gui { var map = BuildLineMap (); - int currentIdx = map.IndexOf(b => Equals (b.Model, o)); + int currentIdx = map.IndexOf (b => Equals (b.Model, o)); if (currentIdx == -1) { return; } - var currentBranch = map.ElementAt(currentIdx); + var currentBranch = map.ElementAt (currentIdx); var next = currentBranch; for (; currentIdx >= 0; currentIdx--) { @@ -905,7 +971,7 @@ namespace Terminal.Gui { // look at next branch up for consideration currentBranch = next; - next = map.ElementAt(currentIdx); + next = map.ElementAt (currentIdx); } // We ran all the way to top of tree @@ -924,13 +990,13 @@ namespace Terminal.Gui { var map = BuildLineMap (); - int currentIdx = map.IndexOf(b => Equals (b.Model, o)); + int currentIdx = map.IndexOf (b => Equals (b.Model, o)); if (currentIdx == -1) { return; } - var currentBranch = map.ElementAt(currentIdx); + var currentBranch = map.ElementAt (currentIdx); var next = currentBranch; for (; currentIdx < map.Count; currentIdx++) { @@ -945,7 +1011,7 @@ namespace Terminal.Gui { // look at next branch for consideration currentBranch = next; - next = map.ElementAt(currentIdx); + next = map.ElementAt (currentIdx); } GoToEnd (); @@ -970,7 +1036,7 @@ namespace Terminal.Gui { // or the current selected branch if (SelectedObject != null) { - idxStart = map.IndexOf(b => Equals (b.Model, SelectedObject)); + idxStart = map.IndexOf (b => Equals (b.Model, SelectedObject)); } // if currently selected object mysteriously vanished, search from beginning @@ -980,9 +1046,9 @@ namespace Terminal.Gui { // loop around all indexes and back to first index for (int idxCur = (idxStart + 1) % map.Count; idxCur != idxStart; idxCur = (idxCur + 1) % map.Count) { - if (predicate (map.ElementAt(idxCur))) { - SelectedObject = map.ElementAt(idxCur).Model; - EnsureVisible (map.ElementAt(idxCur).Model); + if (predicate (map.ElementAt (idxCur))) { + SelectedObject = map.ElementAt (idxCur).Model; + EnsureVisible (map.ElementAt (idxCur).Model); SetNeedsDisplay (); return; } @@ -997,7 +1063,7 @@ namespace Terminal.Gui { { var map = BuildLineMap (); - var idx = map.IndexOf(b => Equals (b.Model, model)); + var idx = map.IndexOf (b => Equals (b.Model, model)); if (idx == -1) { return; @@ -1017,6 +1083,14 @@ namespace Terminal.Gui { } } + /// + /// Expands the currently + /// + public void Expand () + { + Expand (SelectedObject); + } + /// /// Expands the supplied object if it is contained in the tree (either as a root object or /// as an exposed branch object) @@ -1082,6 +1156,14 @@ namespace Terminal.Gui { return ObjectToBranch (o)?.IsExpanded ?? false; } + /// + /// Collapses the + /// + public void Collapse () + { + Collapse (selectedObject); + } + /// /// Collapses the supplied object if it is currently expanded /// @@ -1224,7 +1306,7 @@ namespace Terminal.Gui { return; } - multiSelectedRegions.Push (new TreeSelection (map.ElementAt(0), map.Count, map)); + multiSelectedRegions.Push (new TreeSelection (map.ElementAt (0), map.Count, map)); SetNeedsDisplay (); OnSelectionChanged (new SelectionChangedEventArgs (this, SelectedObject, SelectedObject)); @@ -1258,7 +1340,7 @@ namespace Terminal.Gui { Origin = from; included.Add (Origin.Model); - var oldIdx = map.IndexOf(from); + var oldIdx = map.IndexOf (from); var lowIndex = Math.Min (oldIdx, toIndex); var highIndex = Math.Max (oldIdx, toIndex); diff --git a/UICatalog/KeyBindingsDialog.cs b/UICatalog/KeyBindingsDialog.cs new file mode 100644 index 000000000..cb2c1b9cf --- /dev/null +++ b/UICatalog/KeyBindingsDialog.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Terminal.Gui; + +namespace UICatalog { + + + class KeyBindingsDialog : Dialog { + + + static Dictionary CurrentBindings = new Dictionary(); + private Command[] commands; + private ListView commandsListView; + private Label keyLabel; + + /// + /// Tracks views as they are created in UICatalog so that their keybindings can + /// be managed. + /// + private class ViewTracker { + + public static ViewTracker Instance; + + /// + /// All views seen so far and a bool to indicate if we have applied keybindings to them + /// + Dictionary knownViews = new Dictionary (); + + private object lockKnownViews = new object (); + private Dictionary keybindings; + + public ViewTracker (View top) + { + RecordView (top); + + // Refresh known windows + Application.MainLoop.AddTimeout (TimeSpan.FromMilliseconds (100), (m) => { + + lock (lockKnownViews) { + RecordView (Application.Top); + + ApplyKeyBindingsToAllKnownViews (); + } + + return true; + }); + } + + private void RecordView (View view) + { + if (!knownViews.ContainsKey (view)) { + knownViews.Add (view, false); + } + + // may already have subviews that were added to it + // before we got to it + foreach (var sub in view.Subviews) { + RecordView (sub); + } + + view.Added += RecordView; + } + + internal static void Initialize () + { + Instance = new ViewTracker (Application.Top); + } + + internal void StartUsingNewKeyMap (Dictionary currentBindings) + { + lock (lockKnownViews) { + + // change our knowledge of what keys to bind + this.keybindings = currentBindings; + + // Mark that we have not applied the key bindings yet to any views + foreach (var view in knownViews.Keys) { + knownViews [view] = false; + } + } + } + + private void ApplyKeyBindingsToAllKnownViews () + { + if(keybindings == null) { + return; + } + + // Key is the view Value is whether we have already done it + foreach (var viewDone in knownViews) { + + var view = viewDone.Key; + var done = viewDone.Value; + + if (done) { + // we have already applied keybindings to this view + continue; + } + + var supported = new HashSet(view.GetSupportedCommands ()); + + foreach (var kvp in keybindings) { + + // if the view supports the keybinding + 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); + } + + // mark that we have done this view so don't need to set keybindings again on it + knownViews [view] = true; + } + } + } + } + + public KeyBindingsDialog () : base("Keybindings", 50,10) + { + if(ViewTracker.Instance == null) { + ViewTracker.Initialize (); + } + + // known commands that views can support + commands = Enum.GetValues (typeof (Command)).Cast().ToArray(); + + commandsListView = new ListView (commands) { + Width = Dim.Percent (50), + Height = Dim.Percent (100) - 1, + }; + commandsListView.SelectedItemChanged += CommandsListView_SelectedItemChanged; + Add (commandsListView); + + keyLabel = new Label () { + Text = "Key: None", + Width = Dim.Fill(), + X = Pos.Percent(50), + Y = 0 + }; + Add (keyLabel); + + var btnChange = new Button ("Change") { + X = Pos.Percent (50), + Y = 1, + }; + Add (btnChange); + btnChange.Clicked += RemapKey; + + var close = new Button ("Ok"); + close.Clicked += () => { + Application.RequestStop (); + ViewTracker.Instance.StartUsingNewKeyMap (CurrentBindings); + }; + AddButton (close); + + var cancel = new Button ("Cancel"); + cancel.Clicked += ()=>Application.RequestStop(); + AddButton (cancel); + } + + private void RemapKey () + { + var cmd = commands [commandsListView.SelectedItem]; + Key? key = null; + + // prompt user to hit a key + var dlg = new Dialog ("Enter Key"); + dlg.KeyPress += (k) => { + key = k.KeyEvent.Key; + Application.RequestStop (); + }; + Application.Run (dlg); + + if(key.HasValue) { + CurrentBindings [cmd] = key.Value; + SetTextBoxToShowBinding (cmd); + } + } + + private void SetTextBoxToShowBinding (Command cmd) + { + if (CurrentBindings.ContainsKey (cmd)) { + keyLabel.Text = "Key: " + CurrentBindings [cmd].ToString (); + } else { + keyLabel.Text = "Key: None"; + } + SetNeedsDisplay (); + } + + private void CommandsListView_SelectedItemChanged (ListViewItemEventArgs obj) + { + SetTextBoxToShowBinding ((Command)obj.Value); + } + } +} diff --git a/UICatalog/Scenarios/BackgroundWorkerCollection.cs b/UICatalog/Scenarios/BackgroundWorkerCollection.cs index 02923e146..70e9f1111 100644 --- a/UICatalog/Scenarios/BackgroundWorkerCollection.cs +++ b/UICatalog/Scenarios/BackgroundWorkerCollection.cs @@ -367,7 +367,7 @@ namespace UICatalog.Scenarios { private void OnReportClosed () { - if (Staging.StartStaging != null) { + if (Staging?.StartStaging != null) { ReportClosed?.Invoke (this); } RequestStop (); diff --git a/UICatalog/Scenarios/BordersComparisons.cs b/UICatalog/Scenarios/BordersComparisons.cs index d7c0a97d8..f10886355 100644 --- a/UICatalog/Scenarios/BordersComparisons.cs +++ b/UICatalog/Scenarios/BordersComparisons.cs @@ -41,11 +41,6 @@ namespace UICatalog.Scenarios { X = Pos.Center (), Y = Pos.Center () - 3, }; - var tf2 = new TextField ("1234567890") { - X = Pos.AnchorEnd (10), - Y = Pos.AnchorEnd (1), - Width = 10 - }; var tv = new TextView () { Y = Pos.AnchorEnd (2), Width = 10, @@ -53,7 +48,12 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog, Text = "1234567890" }; - win.Add (tf1, button, label, tf2, tv); + var tf2 = new TextField ("1234567890") { + X = Pos.AnchorEnd (10), + Y = Pos.AnchorEnd (1), + Width = 10 + }; + win.Add (tf1, button, label, tv, tf2); top.Add (win); var top2 = new Border.ToplevelContainer (new Rect (50, 5, 40, 20), @@ -81,11 +81,6 @@ namespace UICatalog.Scenarios { X = Pos.Center (), Y = Pos.Center () - 3, }; - var tf4 = new TextField ("1234567890") { - X = Pos.AnchorEnd (10), - Y = Pos.AnchorEnd (1), - Width = 10 - }; var tv2 = new TextView () { Y = Pos.AnchorEnd (2), Width = 10, @@ -93,7 +88,12 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog, Text = "1234567890" }; - top2.Add (tf3, button2, label2, tf4, tv2); + var tf4 = new TextField ("1234567890") { + X = Pos.AnchorEnd (10), + Y = Pos.AnchorEnd (1), + Width = 10 + }; + top2.Add (tf3, button2, label2, tv2, tf4); top.Add (top2); var frm = new FrameView (new Rect (95, 5, 40, 20), "Test3", null, @@ -118,11 +118,6 @@ namespace UICatalog.Scenarios { X = Pos.Center (), Y = Pos.Center () - 3, }; - var tf6 = new TextField ("1234567890") { - X = Pos.AnchorEnd (10), - Y = Pos.AnchorEnd (1), - Width = 10 - }; var tv3 = new TextView () { Y = Pos.AnchorEnd (2), Width = 10, @@ -130,7 +125,12 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog, Text = "1234567890" }; - frm.Add (tf5, button3, label3, tf6, tv3); + var tf6 = new TextField ("1234567890") { + X = Pos.AnchorEnd (10), + Y = Pos.AnchorEnd (1), + Width = 10 + }; + frm.Add (tf5, button3, label3, tv3, tf6); top.Add (frm); Application.Run (); diff --git a/UICatalog/Scenarios/Text.cs b/UICatalog/Scenarios/Text.cs index 2224f54b5..42b586f93 100644 --- a/UICatalog/Scenarios/Text.cs +++ b/UICatalog/Scenarios/Text.cs @@ -1,5 +1,8 @@ -using System; +using NStack; +using System; +using System.Linq; using System.Text; +using System.Text.RegularExpressions; using Terminal.Gui; using Terminal.Gui.TextValidateProviders; @@ -19,6 +22,14 @@ namespace UICatalog.Scenarios { Width = Dim.Percent (50), //ColorScheme = Colors.Dialog }; + textField.TextChanging += TextField_TextChanging; + + void TextField_TextChanging (TextChangingEventArgs e) + { + textField.Autocomplete.AllSuggestions = Regex.Matches (e.NewText.ToString (), "\\w+") + .Select (s => s.Value) + .Distinct ().ToList (); + } Win.Add (textField); var labelMirroringTextField = new Label (textField.Text) { @@ -40,6 +51,14 @@ namespace UICatalog.Scenarios { ColorScheme = Colors.Dialog }; textView.Text = s; + textView.DrawContent += TextView_DrawContent; + + void TextView_DrawContent (Rect e) + { + textView.Autocomplete.AllSuggestions = Regex.Matches (textView.Text.ToString (), "\\w+") + .Select (s => s.Value) + .Distinct ().ToList (); + } Win.Add (textView); var labelMirroringTextView = new Label (textView.Text) { diff --git a/UICatalog/Scenarios/TextViewAutocompletePopup.cs b/UICatalog/Scenarios/TextViewAutocompletePopup.cs new file mode 100644 index 000000000..8bde07405 --- /dev/null +++ b/UICatalog/Scenarios/TextViewAutocompletePopup.cs @@ -0,0 +1,186 @@ +using System.Linq; +using System.Text.RegularExpressions; +using Terminal.Gui; + +namespace UICatalog.Scenarios { + [ScenarioMetadata (Name: "TextView Autocomplete Popup", Description: "Show five TextView Autocomplete Popup effects")] + [ScenarioCategory ("Controls")] + public class TextViewAutocompletePopup : Scenario { + + TextView textViewTopLeft; + TextView textViewTopRight; + TextView textViewBottomLeft; + TextView textViewBottomRight; + TextView textViewCentered; + MenuItem miMultiline; + MenuItem miWrap; + StatusItem siMultiline; + StatusItem siWrap; + int height = 10; + + public override void Setup () + { + Win.Title = GetName (); + var width = 20; + var colorScheme = Colors.Dialog; + var text = " jamp jemp jimp jomp jump"; + + var menu = new MenuBar (new MenuBarItem [] { + new MenuBarItem ("_File", new MenuItem [] { + miMultiline = new MenuItem ("_Multiline", "", () => Multiline()){CheckType = MenuItemCheckStyle.Checked}, + miWrap = new MenuItem ("_Word Wrap", "", () => WordWrap()){CheckType = MenuItemCheckStyle.Checked}, + new MenuItem ("_Quit", "", () => Quit()) + }) + }); + Top.Add (menu); + + textViewTopLeft = new TextView () { + Width = width, + Height = height, + ColorScheme = colorScheme, + Text = text + }; + textViewTopLeft.DrawContent += TextViewTopLeft_DrawContent; + Win.Add (textViewTopLeft); + + textViewTopRight = new TextView () { + X = Pos.AnchorEnd (width), + Width = width, + Height = height, + ColorScheme = colorScheme, + Text = text + }; + textViewTopRight.DrawContent += TextViewTopRight_DrawContent; + Win.Add (textViewTopRight); + + textViewBottomLeft = new TextView () { + Y = Pos.AnchorEnd (height), + Width = width, + Height = height, + ColorScheme = colorScheme, + Text = text + }; + textViewBottomLeft.DrawContent += TextViewBottomLeft_DrawContent; + Win.Add (textViewBottomLeft); + + textViewBottomRight = new TextView () { + X = Pos.AnchorEnd (width), + Y = Pos.AnchorEnd (height), + Width = width, + Height = height, + ColorScheme = colorScheme, + Text = text + }; + textViewBottomRight.DrawContent += TextViewBottomRight_DrawContent; + Win.Add (textViewBottomRight); + + textViewCentered = new TextView () { + X = Pos.Center (), + Y = Pos.Center (), + Width = width, + Height = height, + ColorScheme = colorScheme, + Text = text + }; + textViewCentered.DrawContent += TextViewCentered_DrawContent; + Win.Add (textViewCentered); + + miMultiline.Checked = textViewTopLeft.Multiline; + miWrap.Checked = textViewTopLeft.WordWrap; + + var statusBar = new StatusBar (new StatusItem [] { + new StatusItem(Key.CtrlMask | Key.Q, "~^Q~ Quit", () => Quit()), + siMultiline = new StatusItem(Key.Null, "", null), + siWrap = new StatusItem(Key.Null, "", null) + }); + Top.Add (statusBar); + + Win.LayoutStarted += Win_LayoutStarted; + } + + private void Win_LayoutStarted (View.LayoutEventArgs obj) + { + miMultiline.Checked = textViewTopLeft.Multiline; + miWrap.Checked = textViewTopLeft.WordWrap; + SetMultilineStatusText (); + SetWrapStatusText (); + + if (miMultiline.Checked) { + height = 10; + } else { + height = 1; + } + textViewBottomLeft.Y = textViewBottomRight.Y = Pos.AnchorEnd (height); + } + + private void SetMultilineStatusText () + { + siMultiline.Title = $"Multiline: {miMultiline.Checked}"; + } + + private void SetWrapStatusText () + { + siWrap.Title = $"WordWrap: {miWrap.Checked}"; + } + + private void SetAllSuggestions (TextView view) + { + view.Autocomplete.AllSuggestions = Regex.Matches (view.Text.ToString (), "\\w+") + .Select (s => s.Value) + .Distinct ().ToList (); + } + + private void TextViewCentered_DrawContent (Rect obj) + { + SetAllSuggestions (textViewCentered); + } + + private void TextViewBottomRight_DrawContent (Rect obj) + { + SetAllSuggestions (textViewBottomRight); + } + + private void TextViewBottomLeft_DrawContent (Rect obj) + { + SetAllSuggestions (textViewBottomLeft); + } + + private void TextViewTopRight_DrawContent (Rect obj) + { + SetAllSuggestions (textViewTopRight); + } + + private void TextViewTopLeft_DrawContent (Rect obj) + { + SetAllSuggestions (textViewTopLeft); + } + + private void Multiline () + { + miMultiline.Checked = !miMultiline.Checked; + SetMultilineStatusText (); + textViewTopLeft.Multiline = miMultiline.Checked; + textViewTopRight.Multiline = miMultiline.Checked; + textViewBottomLeft.Multiline = miMultiline.Checked; + textViewBottomRight.Multiline = miMultiline.Checked; + textViewCentered.Multiline = miMultiline.Checked; + } + + private void WordWrap () + { + miWrap.Checked = !miWrap.Checked; + textViewTopLeft.WordWrap = miWrap.Checked; + textViewTopRight.WordWrap = miWrap.Checked; + textViewBottomLeft.WordWrap = miWrap.Checked; + textViewBottomRight.WordWrap = miWrap.Checked; + textViewCentered.WordWrap = miWrap.Checked; + miWrap.Checked = textViewTopLeft.WordWrap; + SetWrapStatusText (); + } + + private void Quit () + { + Application.RequestStop (); + } + } +} diff --git a/UICatalog/UICatalog.cs b/UICatalog/UICatalog.cs index ee011a0bf..05c5c7db3 100644 --- a/UICatalog/UICatalog.cs +++ b/UICatalog/UICatalog.cs @@ -313,6 +313,7 @@ namespace UICatalog { menuItems.Add (CreateSizeStyle ()); menuItems.Add (CreateAlwaysSetPosition ()); menuItems.Add (CreateDisabledEnabledMouse ()); + menuItems.Add (CreateKeybindings ()); return menuItems; } @@ -331,6 +332,22 @@ namespace UICatalog { return menuItems.ToArray (); } + private static MenuItem[] CreateKeybindings() + { + + List menuItems = new List (); + var item = new MenuItem (); + item.Title = "Keybindings"; + item.Action += () => { + var dlg = new KeyBindingsDialog (); + Application.Run (dlg); + }; + + menuItems.Add (null); + menuItems.Add (item); + + return menuItems.ToArray (); + } static MenuItem [] CreateAlwaysSetPosition () { diff --git a/UnitTests/ApplicationTests.cs b/UnitTests/ApplicationTests.cs index f10338f97..746523a2d 100644 --- a/UnitTests/ApplicationTests.cs +++ b/UnitTests/ApplicationTests.cs @@ -369,6 +369,15 @@ namespace Terminal.Gui.Core { 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 (); } diff --git a/UnitTests/AutocompleteTests.cs b/UnitTests/AutocompleteTests.cs index 4eaf4908d..783546b05 100644 --- a/UnitTests/AutocompleteTests.cs +++ b/UnitTests/AutocompleteTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Terminal.Gui; using Xunit; @@ -9,21 +10,21 @@ using Xunit; namespace Terminal.Gui.Core { public class AutocompleteTests { - [Fact][AutoInitShutdown] - public void Test_GenerateSuggestions_Simple() + [Fact] + public void Test_GenerateSuggestions_Simple () { - var ac = new Autocomplete (); - ac.AllSuggestions = new List { "fish","const","Cobble"}; + var ac = new TextViewAutocomplete (); + ac.AllSuggestions = new List { "fish", "const", "Cobble" }; var tv = new TextView (); tv.InsertText ("co"); - ac.GenerateSuggestions (tv); + ac.HostControl = tv; + ac.GenerateSuggestions (); Assert.Equal (2, ac.Suggestions.Count); - Assert.Equal ("const", ac.Suggestions[0]); - Assert.Equal ("Cobble", ac.Suggestions[1]); - + Assert.Equal ("const", ac.Suggestions [0]); + Assert.Equal ("Cobble", ac.Suggestions [1]); } [Fact] @@ -41,7 +42,7 @@ namespace Terminal.Gui.Core { Focus = Application.Driver.MakeAttribute (Color.Black, Color.Cyan), }; - // should be seperate instance + // should be separate instance Assert.NotSame (Colors.Menu, tv.Autocomplete.ColorScheme); // with the values we set on it @@ -50,8 +51,105 @@ namespace Terminal.Gui.Core { Assert.Equal (Color.Black, tv.Autocomplete.ColorScheme.Focus.Foreground); Assert.Equal (Color.Cyan, tv.Autocomplete.ColorScheme.Focus.Background); + } + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var tv = new TextView () { + Width = 10, + Height = 2, + Text = " Fortunately super feature." + }; + var top = Application.Top; + top.Add (tv); + Application.Begin (top); + Assert.Equal (Point.Empty, tv.CursorPosition); + Assert.NotNull (tv.Autocomplete); + Assert.Empty (tv.Autocomplete.AllSuggestions); + tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+") + .Select (s => s.Value) + .Distinct ().ToList (); + Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count); + Assert.Equal ("Fortunately", tv.Autocomplete.AllSuggestions [0]); + Assert.Equal ("super", tv.Autocomplete.AllSuggestions [1]); + Assert.Equal ("feature", tv.Autocomplete.AllSuggestions [^1]); + Assert.Equal (0, tv.Autocomplete.SelectedIdx); + Assert.Empty (tv.Autocomplete.Suggestions); + Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ()))); + top.Redraw (tv.Bounds); + 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 ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (0, tv.Autocomplete.SelectedIdx); + Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + top.Redraw (tv.Bounds); + 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 ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (1, tv.Autocomplete.SelectedIdx); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + top.Redraw (tv.Bounds); + 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 ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (0, tv.Autocomplete.SelectedIdx); + Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + top.Redraw (tv.Bounds); + 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 ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (1, tv.Autocomplete.SelectedIdx); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + top.Redraw (tv.Bounds); + 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 ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (0, tv.Autocomplete.SelectedIdx); + Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.Autocomplete.Visible); + top.Redraw (tv.Bounds); + Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ()))); + Assert.Equal ($"F Fortunately super feature.", tv.Text); + Assert.Equal (new Point (1, 0), tv.CursorPosition); + Assert.Empty (tv.Autocomplete.Suggestions); + Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count); + Assert.False (tv.Autocomplete.Visible); + top.Redraw (tv.Bounds); + Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.Reopen, new KeyModifiers ()))); + 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, tv.Autocomplete.AllSuggestions.Count); + Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.SelectionKey, new KeyModifiers ()))); + Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text); + Assert.Equal (new Point (11, 0), tv.CursorPosition); + Assert.Equal (2, tv.Autocomplete.Suggestions.Count); + Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [0]); + Assert.Equal ("feature", tv.Autocomplete.Suggestions [^1]); + Assert.Equal (0, tv.Autocomplete.SelectedIdx); + Assert.Equal ("Fortunately", tv.Autocomplete.Suggestions [tv.Autocomplete.SelectedIdx]); + Assert.True (tv.ProcessKey (new KeyEvent (tv.Autocomplete.CloseKey, new KeyModifiers ()))); + Assert.Equal ($"Fortunately Fortunately super feature.", tv.Text); + Assert.Equal (new Point (11, 0), tv.CursorPosition); + Assert.Empty (tv.Autocomplete.Suggestions); + Assert.Equal (3, tv.Autocomplete.AllSuggestions.Count); } } -} +} \ No newline at end of file diff --git a/UnitTests/ButtonTests.cs b/UnitTests/ButtonTests.cs new file mode 100644 index 000000000..a9f233498 --- /dev/null +++ b/UnitTests/ButtonTests.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class ButtonTests { + [Fact] + public void Constructors_Defaults () + { + var btn = new Button (); + Assert.Equal (string.Empty, btn.Text); + Assert.Equal ("[ ]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ()); + Assert.False (btn.IsDefault); + Assert.Equal (TextAlignment.Centered, btn.TextAlignment); + Assert.Equal ('_', btn.HotKeySpecifier); + Assert.True (btn.CanFocus); + Assert.Equal (new Rect (0, 0, 4, 1), btn.Frame); + Assert.Equal (Key.Null, btn.HotKey); + + btn = new Button ("Test", true); + Assert.Equal ("Test", btn.Text); + Assert.Equal ("[< Test >]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ()); + Assert.True (btn.IsDefault); + Assert.Equal (TextAlignment.Centered, btn.TextAlignment); + Assert.Equal ('_', btn.HotKeySpecifier); + Assert.True (btn.CanFocus); + Assert.Equal (new Rect (0, 0, 10, 1), btn.Frame); + Assert.Equal (Key.Null, btn.HotKey); + + btn = new Button (3, 4, "Test", true); + Assert.Equal ("Test", btn.Text); + Assert.Equal ("[< Test >]", btn.GetType ().BaseType.GetProperty ("Text").GetValue (btn).ToString ()); + Assert.True (btn.IsDefault); + Assert.Equal (TextAlignment.Centered, btn.TextAlignment); + Assert.Equal ('_', btn.HotKeySpecifier); + Assert.True (btn.CanFocus); + Assert.Equal (new Rect (3, 4, 10, 1), btn.Frame); + Assert.Equal (Key.Null, btn.HotKey); + } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var clicked = false; + Button btn = new Button ("Test"); + btn.Clicked += () => clicked = true; + Application.Top.Add (btn); + Application.Begin (Application.Top); + + Assert.Equal (Key.T, btn.HotKey); + Assert.False (btn.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ()))); + Assert.False (clicked); + Assert.True (btn.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (clicked); + clicked = false; + Assert.False (btn.IsDefault); + Assert.False (btn.ProcessColdKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.False (clicked); + btn.IsDefault = true; + Assert.True (btn.ProcessColdKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (clicked); + clicked = false; + Assert.True (btn.ProcessColdKey (new KeyEvent (Key.AltMask | Key.T, new KeyModifiers ()))); + Assert.True (clicked); + clicked = false; + Assert.True (btn.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (clicked); + clicked = false; + Assert.True (btn.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()))); + Assert.True (clicked); + clicked = false; + Assert.True (btn.ProcessKey (new KeyEvent ((Key)'t', new KeyModifiers ()))); + Assert.True (clicked); + } + + + [Fact] + [AutoInitShutdown] + public void ChangeHotKey () + { + var clicked = false; + Button btn = new Button ("Test"); + btn.Clicked += () => clicked = true; + Application.Top.Add (btn); + Application.Begin (Application.Top); + + Assert.Equal (Key.T, btn.HotKey); + Assert.False (btn.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ()))); + Assert.False (clicked); + Assert.True (btn.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (clicked); + clicked = false; + + btn.HotKey = Key.E; + Assert.True (btn.ProcessHotKey (new KeyEvent (Key.E | Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (clicked); + } + } +} diff --git a/UnitTests/CheckboxTests.cs b/UnitTests/CheckboxTests.cs new file mode 100644 index 000000000..647d4de68 --- /dev/null +++ b/UnitTests/CheckboxTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class CheckboxTests { + [Fact] + public void Constructors_Defaults () + { + var ckb = new CheckBox (); + Assert.False (ckb.Checked); + Assert.Equal (string.Empty, ckb.Text); + Assert.True (ckb.CanFocus); + Assert.Equal (new Rect (0, 0, 4, 1), ckb.Frame); + + ckb = new CheckBox ("Test", true); + Assert.True (ckb.Checked); + Assert.Equal ("Test", ckb.Text); + Assert.True (ckb.CanFocus); + Assert.Equal (new Rect (0, 0, 8, 1), ckb.Frame); + + ckb = new CheckBox (1, 2, "Test"); + Assert.False (ckb.Checked); + Assert.Equal ("Test", ckb.Text); + Assert.True (ckb.CanFocus); + Assert.Equal (new Rect (1, 2, 8, 1), ckb.Frame); + + ckb = new CheckBox (3, 4, "Test", true); + Assert.True (ckb.Checked); + Assert.Equal ("Test", ckb.Text); + Assert.True (ckb.CanFocus); + Assert.Equal (new Rect (3, 4, 8, 1), ckb.Frame); + } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var isChecked = false; + CheckBox ckb = new CheckBox ("Test"); + ckb.Toggled += (e) => isChecked = true; + Application.Top.Add (ckb); + Application.Begin (Application.Top); + + Assert.Equal (Key.Null, ckb.HotKey); + Assert.False (ckb.ProcessHotKey (new KeyEvent (Key.T, new KeyModifiers ()))); + Assert.False (isChecked); + ckb.Text = "_Test"; + Assert.Equal (Key.T, ckb.HotKey); + Assert.True (ckb.ProcessHotKey (new KeyEvent (Key.T | Key.AltMask, new KeyModifiers () { Alt = true }))); + Assert.True (isChecked); + isChecked = false; + Assert.True (ckb.ProcessKey (new KeyEvent ((Key)' ', new KeyModifiers ()))); + Assert.True (isChecked); + isChecked = false; + Assert.True (ckb.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()))); + Assert.True (isChecked); + } + } +} diff --git a/UnitTests/ComboBoxTests.cs b/UnitTests/ComboBoxTests.cs index 3d6fc0095..43ef8aaba 100644 --- a/UnitTests/ComboBoxTests.cs +++ b/UnitTests/ComboBoxTests.cs @@ -1,24 +1,190 @@ using System; +using System.Collections.Generic; using System.Linq; using Terminal.Gui; using Xunit; namespace Terminal.Gui.Views { - public class ComboBoxTests { - [Fact] - [AutoInitShutdown] - public void EnsureKeyEventsDoNotCauseExceptions () - { - var comboBox = new ComboBox ("0"); + public class ComboBoxTests { + [Fact] + public void Constructors_Defaults () + { + var cb = new ComboBox (); + Assert.Equal (string.Empty, cb.Text); + Assert.Null (cb.Source); - var source = Enumerable.Range (0, 15).Select (x => x.ToString ()).ToArray (); - comboBox.SetSource(source); + cb = new ComboBox ("Test"); + Assert.Equal ("Test", cb.Text); + Assert.Null (cb.Source); - Application.Top.Add(comboBox); + cb = new ComboBox (new Rect (1, 2, 10, 20), new List () { "One", "Two", "Three" }); + Assert.Equal (string.Empty, cb.Text); + Assert.NotNull (cb.Source); + } - foreach (var key in (Key [])Enum.GetValues (typeof(Key))) { - comboBox.ProcessKey (new KeyEvent (key, new KeyModifiers ())); - } - } - } + [Fact] + [AutoInitShutdown] + public void EnsureKeyEventsDoNotCauseExceptions () + { + var comboBox = new ComboBox ("0"); + + var source = Enumerable.Range (0, 15).Select (x => x.ToString ()).ToArray (); + comboBox.SetSource (source); + + Application.Top.Add (comboBox); + + foreach (var key in (Key [])Enum.GetValues (typeof (Key))) { + Assert.Null (Record.Exception (() => comboBox.ProcessKey (new KeyEvent (key, new KeyModifiers ())))); + } + } + + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + List source = new List () { "One", "Two", "Three" }; + ComboBox cb = new ComboBox (); + cb.SetSource (source); + Application.Top.Add (cb); + Application.Top.FocusFirst (); + Assert.Equal (-1, cb.SelectedItem); + Assert.Equal(string.Empty,cb.Text); + var opened = false; + cb.OpenSelectedItem += (_) => opened = true; + Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.False (opened); + cb.Text = "Tw"; + Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (opened); + Assert.Equal ("Two", cb.Text); + cb.SetSource (null); + Assert.False (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // with no source also expand empty + Assert.True (cb.IsShow); + Assert.Equal (-1, cb.SelectedItem); + cb.SetSource(source); + cb.Text = ""; + Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // collapse + Assert.False (cb.IsShow); + Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); // expand + Assert.True (cb.IsShow); + cb.Collapse (); + Assert.False (cb.IsShow); + Assert.True (cb.HasFocus); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); // losing focus + Assert.False (cb.IsShow); + Assert.False (cb.HasFocus); + Application.Top.FocusFirst (); // Gets focus again + Assert.False (cb.IsShow); + Assert.True (cb.HasFocus); + cb.Expand (); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (1, cb.SelectedItem); + Assert.Equal ("Two", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (2, cb.SelectedItem); + Assert.Equal ("Three", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (2, cb.SelectedItem); + Assert.Equal ("Three", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (1, cb.SelectedItem); + Assert.Equal ("Two", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (1, cb.SelectedItem); + Assert.Equal ("Two", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (2, cb.SelectedItem); + Assert.Equal ("Three", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ()))); + Assert.False (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); // losing focus + Assert.False (cb.HasFocus); + Assert.False (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Application.Top.FocusFirst (); // Gets focus again + Assert.True (cb.HasFocus); + Assert.False (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.U | Key.CtrlMask, new KeyModifiers ()))); + Assert.True (cb.HasFocus); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("", cb.Text); + Assert.Equal (3, cb.Source.Count); + } + + [Fact] + [AutoInitShutdown] + public void Source_Equal_Null_Or_Count_Equal_Zero_Sets_SelectedItem_Equal_To_Minus_One () + { + var cb = new ComboBox (); + Application.Top.Add (cb); + Application.Top.FocusFirst (); + Assert.Null(cb.Source); + Assert.Equal (-1, cb.SelectedItem); + var source = new List (); + cb.SetSource(source); + Assert.NotNull (cb.Source); + Assert.Equal (0, cb.Source.Count); + Assert.Equal (-1, cb.SelectedItem); + source.Add ("One"); + Assert.Equal (1, cb.Source.Count); + Assert.Equal (-1, cb.SelectedItem); + Assert.True (cb.ProcessKey (new KeyEvent (Key.F4, new KeyModifiers ()))); + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + source.Add ("Two"); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("One", cb.Text); + cb.Text = "T"; + Assert.True (cb.IsShow); + Assert.Equal (0, cb.SelectedItem); + Assert.Equal ("T", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.False (cb.IsShow); + Assert.Equal (2, cb.Source.Count); + Assert.Equal (1, cb.SelectedItem); + Assert.Equal ("Two", cb.Text); + Assert.True (cb.ProcessKey (new KeyEvent (Key.Esc, new KeyModifiers ()))); + Assert.False (cb.IsShow); + Assert.Equal (1, cb.SelectedItem); // retains last accept selected item + Assert.Equal ("", cb.Text); // clear text + cb.SetSource(new List ()); + Assert.Equal (0, cb.Source.Count); + Assert.Equal (-1, cb.SelectedItem); + Assert.Equal ("", cb.Text); + } + } } \ No newline at end of file diff --git a/UnitTests/DateFieldTests.cs b/UnitTests/DateFieldTests.cs new file mode 100644 index 000000000..9a5460d0a --- /dev/null +++ b/UnitTests/DateFieldTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class DateFieldTests { + [Fact] + public void Constructors_Defaults () + { + var df = new DateField (); + Assert.False (df.IsShortFormat); + Assert.Equal (DateTime.MinValue, df.Date); + Assert.Equal (1, df.CursorPosition); + Assert.Equal (new Rect (0, 0, 12, 1), df.Frame); + + var date = DateTime.Now; + df = new DateField (date); + Assert.False (df.IsShortFormat); + Assert.Equal (date, df.Date); + Assert.Equal (1, df.CursorPosition); + Assert.Equal (new Rect (0, 0, 12, 1), df.Frame); + + df = new DateField (1, 2, date); + Assert.False (df.IsShortFormat); + Assert.Equal (date, df.Date); + Assert.Equal (1, df.CursorPosition); + Assert.Equal (new Rect (1, 2, 12, 1), df.Frame); + + df = new DateField (3, 4, date, true); + Assert.True (df.IsShortFormat); + Assert.Equal (date, df.Date); + Assert.Equal (1, df.CursorPosition); + Assert.Equal (new Rect (3, 4, 10, 1), df.Frame); + + df.IsShortFormat = false; + Assert.Equal (new Rect (3, 4, 12, 1), df.Frame); + Assert.Equal (12, df.Width); + } + + [Fact] + public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format () + { + var df = new DateField (); + Assert.Equal (1, df.CursorPosition); + df.CursorPosition = 0; + Assert.Equal (1, df.CursorPosition); + df.CursorPosition = 11; + Assert.Equal (10, df.CursorPosition); + df.IsShortFormat = true; + df.CursorPosition = 0; + Assert.Equal (1, df.CursorPosition); + df.CursorPosition = 9; + Assert.Equal (8, df.CursorPosition); + } + + [Fact] + public void KeyBindings_Command () + { + DateField df = new DateField (DateTime.Parse ("12/12/1971")); + df.ReadOnly = true; + Assert.True (df.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ()))); + Assert.Equal (" 12/12/1971", df.Text); + df.ReadOnly = false; + Assert.True (df.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (" 02/12/1971", df.Text); + df.CursorPosition = 4; + df.ReadOnly = true; + Assert.True (df.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ()))); + Assert.Equal (" 02/12/1971", df.Text); + df.ReadOnly = false; + Assert.True (df.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()))); + Assert.Equal (" 02/02/1971", df.Text); + Assert.True (df.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (1, df.CursorPosition); + Assert.True (df.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (10, df.CursorPosition); + Assert.True (df.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (1, df.CursorPosition); + Assert.True (df.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (10, df.CursorPosition); + Assert.True (df.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (9, df.CursorPosition); + Assert.True (df.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (10, df.CursorPosition); + Assert.False (df.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ()))); + df.ReadOnly = true; + df.CursorPosition = 1; + Assert.True (df.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Equal (" 02/02/1971", df.Text); + df.ReadOnly = false; + Assert.True (df.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Equal (" 12/02/1971", df.Text); + } + } +} diff --git a/UnitTests/FrameViewTests.cs b/UnitTests/FrameViewTests.cs new file mode 100644 index 000000000..11b704338 --- /dev/null +++ b/UnitTests/FrameViewTests.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class FrameViewTests { + [Fact] + public void Constuctors_Defaults () + { + var fv = new FrameView (); + Assert.Equal (string.Empty, fv.Title); + Assert.Equal (string.Empty, fv.Text); + Assert.NotNull (fv.Border); + Assert.Single (fv.InternalSubviews); + Assert.Single (fv.Subviews); + + fv = new FrameView ("Test"); + Assert.Equal ("Test", fv.Title); + Assert.Equal (string.Empty, fv.Text); + Assert.NotNull (fv.Border); + Assert.Single (fv.InternalSubviews); + Assert.Single (fv.Subviews); + + fv = new FrameView (new Rect (1, 2, 10, 20), "Test"); + Assert.Equal ("Test", fv.Title); + Assert.Equal (string.Empty, fv.Text); + Assert.NotNull (fv.Border); + Assert.Single (fv.InternalSubviews); + Assert.Single (fv.Subviews); + Assert.Equal (new Rect (1, 2, 10, 20), fv.Frame); + } + } +} diff --git a/UnitTests/HexViewTests.cs b/UnitTests/HexViewTests.cs index 89d1dbaf8..5f54d4036 100644 --- a/UnitTests/HexViewTests.cs +++ b/UnitTests/HexViewTests.cs @@ -395,5 +395,63 @@ namespace Terminal.Gui.Views { Assert.Equal ("Test", Encoding.Default.GetString (readBuffer)); Assert.Equal (Encoding.Default.GetString (buffer), Encoding.Default.GetString (readBuffer)); } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var hv = new HexView (LoadStream ()) { Width = 20, Height = 10 }; + Application.Top.Add (hv); + Application.Begin (Application.Top); + + Assert.Equal (63, hv.Source.Length); + Assert.Equal (1, hv.Position); + Assert.Equal (4, hv.BytesPerLine); + + // right side only needed to press one time + Assert.True (hv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (2, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (5, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (41, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent ('v' + Key.AltMask, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (41, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (64, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorRight | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (4, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorDown | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (37, hv.Position); + + Assert.True (hv.ProcessKey (new KeyEvent (Key.CursorUp | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (1, hv.Position); + } } } diff --git a/UnitTests/KeyTests.cs b/UnitTests/KeyTests.cs index 4585ebd82..455cfa992 100644 --- a/UnitTests/KeyTests.cs +++ b/UnitTests/KeyTests.cs @@ -135,5 +135,38 @@ namespace Terminal.Gui.Core { 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 ()); + } } -} +} \ No newline at end of file diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs new file mode 100644 index 000000000..1993e4092 --- /dev/null +++ b/UnitTests/ListViewTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class ListViewTests { + [Fact] + public void Constructors_Defaults () + { + var lv = new ListView (); + Assert.Null (lv.Source); + Assert.True (lv.CanFocus); + + lv = new ListView (new List () { "One", "Two", "Three" }); + Assert.NotNull (lv.Source); + + lv = new ListView (new NewListDataSource()); + Assert.NotNull (lv.Source); + + lv = new ListView (new Rect (0, 1, 10, 20), new List () { "One", "Two", "Three" }); + Assert.NotNull (lv.Source); + Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame); + + lv = new ListView (new Rect (0, 1, 10, 20), new NewListDataSource ()); + Assert.NotNull (lv.Source); + Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame); + } + + private class NewListDataSource : IListDataSource { + public int Count => throw new NotImplementedException (); + + public int Length => throw new NotImplementedException (); + + public bool IsMarked (int item) + { + throw new NotImplementedException (); + } + + public void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0) + { + throw new NotImplementedException (); + } + + public void SetMark (int item, bool value) + { + throw new NotImplementedException (); + } + + public IList ToList () + { + throw new NotImplementedException (); + } + } + + [Fact] + public void KeyBindings_Command () + { + List source = new List () { "One", "Two", "Three" }; + ListView lv = new ListView (source) { Height = 2, AllowsMarking = true }; + Assert.Equal (0, lv.SelectedItem); + Assert.True (lv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (1, lv.SelectedItem); + Assert.True (lv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (0, lv.SelectedItem); + Assert.True (lv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (2, lv.SelectedItem); + Assert.Equal (2, lv.TopItem); + Assert.True (lv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.Equal (0, lv.SelectedItem); + Assert.Equal (0, lv.TopItem); + Assert.False (lv.Source.IsMarked (lv.SelectedItem)); + Assert.True (lv.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()))); + Assert.True (lv.Source.IsMarked (lv.SelectedItem)); + var opened = false; + lv.OpenSelectedItem += (_) => opened = true; + Assert.True (lv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.True (opened); + Assert.True (lv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (2, lv.SelectedItem); + Assert.True (lv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (0, lv.SelectedItem); + } + } +} diff --git a/UnitTests/MenuTests.cs b/UnitTests/MenuTests.cs index e15eb4424..d5489be0f 100644 --- a/UnitTests/MenuTests.cs +++ b/UnitTests/MenuTests.cs @@ -183,7 +183,6 @@ namespace Terminal.Gui.Views { Assert.Equal ("_Paste", miCurrent.Title); for (int i = 2; i >= -1; i--) { - View view; if (i == -1) { Assert.False (mCurrent.MouseEvent (new MouseEvent () { X = 10, @@ -301,7 +300,9 @@ namespace Terminal.Gui.Views { Assert.Equal ("_File", GetCurrentMenuBarItemTitle ()); Assert.Equal ("_New", GetCurrentMenuTitle ()); - Assert.True (mCurrent.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.False (mCurrent.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.True (menu.IsMenuOpen); + Assert.True (Application.Top.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); Assert.False (menu.IsMenuOpen); Assert.Equal ("Closed", GetCurrentMenuBarItemTitle ()); Assert.Equal ("None", GetCurrentMenuTitle ()); diff --git a/UnitTests/PosTests.cs b/UnitTests/PosTests.cs index aca856084..70c12468c 100644 --- a/UnitTests/PosTests.cs +++ b/UnitTests/PosTests.cs @@ -47,6 +47,134 @@ namespace Terminal.Gui.Core { Assert.NotEqual (pos1, pos2); } + [Fact] + [AutoInitShutdown] + public void AnchorEnd_Equal_Inside_Window () + { + var viewWidth = 10; + var viewHeight = 1; + var tv = new TextView () { + X = Pos.AnchorEnd (viewWidth), + Y = Pos.AnchorEnd (viewHeight), + Width = viewWidth, + Height = viewHeight + }; + + var win = new Window (); + + win.Add (tv); + + var top = Application.Top; + top.Add (win); + Application.Begin (top); + + Assert.Equal (new Rect (0, 0, 80, 25), top.Frame); + Assert.Equal (new Rect (0, 0, 80, 25), win.Frame); + Assert.Equal (new Rect (1, 1, 78, 23), win.Subviews[0].Frame); + Assert.Equal ("ContentView()({X=1,Y=1,Width=78,Height=23})", win.Subviews [0].ToString()); + Assert.Equal (new Rect (1, 1, 79, 24), new Rect ( + win.Subviews[0].Frame.Left, win.Subviews [0].Frame.Top, + win.Subviews [0].Frame.Right, win.Subviews[0].Frame.Bottom)); + Assert.Equal (new Rect (68, 22, 10, 1), tv.Frame); + } + + [Fact] + [AutoInitShutdown] + public void AnchorEnd_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel () + { + var viewWidth = 10; + var viewHeight = 1; + var tv = new TextView () { + X = Pos.AnchorEnd (viewWidth), + Y = Pos.AnchorEnd (viewHeight), + Width = viewWidth, + Height = viewHeight + }; + + var win = new Window (); + + win.Add (tv); + + var menu = new MenuBar (); + var status = new StatusBar (); + var top = Application.Top; + top.Add (win, menu, status); + Application.Begin (top); + + Assert.Equal (new Rect (0, 0, 80, 25), top.Frame); + Assert.Equal (new Rect (0, 0, 80, 1), menu.Frame); + Assert.Equal (new Rect (0, 24, 80, 1), status.Frame); + Assert.Equal (new Rect (0, 1, 80, 23), win.Frame); + Assert.Equal (new Rect (1, 1, 78, 21), win.Subviews [0].Frame); + Assert.Equal (new Rect (1, 1, 79, 22), new Rect ( + win.Subviews [0].Frame.Left, win.Subviews [0].Frame.Top, + win.Subviews [0].Frame.Right, win.Subviews [0].Frame.Bottom)); + Assert.Equal (new Rect (68, 20, 10, 1), tv.Frame); + } + + [Fact] + [AutoInitShutdown] + public void Bottom_Equal_Inside_Window () + { + var win = new Window (); + + var label = new Label ("This should be the last line.") { + TextAlignment = Terminal.Gui.TextAlignment.Centered, + ColorScheme = Colors.Menu, + Width = Dim.Fill (), + X = Pos.Center (), + Y = Pos.Bottom (win) - 4 // two lines top border more two lines above border + }; + + win.Add (label); + + var top = Application.Top; + top.Add (win); + Application.Begin (top); + + Assert.Equal (new Rect (0, 0, 80, 25), top.Frame); + Assert.Equal (new Rect (0, 0, 80, 25), win.Frame); + Assert.Equal (new Rect (1, 1, 78, 23), win.Subviews [0].Frame); + Assert.Equal ("ContentView()({X=1,Y=1,Width=78,Height=23})", win.Subviews [0].ToString ()); + Assert.Equal (new Rect (0, 0, 80, 25), new Rect ( + win.Frame.Left, win.Frame.Top, + win.Frame.Right, win.Frame.Bottom)); + Assert.Equal (new Rect (0, 21, 78, 1), label.Frame); + } + + [Fact] + [AutoInitShutdown] + public void Bottom_Equal_Inside_Window_With_MenuBar_And_StatusBar_On_Toplevel () + { + var win = new Window (); + + var label = new Label ("This should be the last line.") { + TextAlignment = Terminal.Gui.TextAlignment.Centered, + ColorScheme = Colors.Menu, + Width = Dim.Fill (), + X = Pos.Center (), + Y = Pos.Bottom (win) - 4 // two lines top border more two lines above border + }; + + win.Add (label); + + var menu = new MenuBar (); + var status = new StatusBar (); + var top = Application.Top; + top.Add (win, menu, status); + Application.Begin (top); + + Assert.Equal (new Rect (0, 0, 80, 25), top.Frame); + Assert.Equal (new Rect (0, 0, 80, 1), menu.Frame); + Assert.Equal (new Rect (0, 24, 80, 1), status.Frame); + Assert.Equal (new Rect (0, 1, 80, 23), win.Frame); + Assert.Equal (new Rect (1, 1, 78, 21), win.Subviews [0].Frame); + Assert.Equal (new Rect (0, 1, 80, 24), new Rect ( + win.Frame.Left, win.Frame.Top, + win.Frame.Right, win.Frame.Bottom)); + Assert.Equal (new Rect (0, 20, 78, 1), label.Frame); + } + [Fact] public void AnchorEnd_Negative_Throws () { diff --git a/UnitTests/RadioGroupTests.cs b/UnitTests/RadioGroupTests.cs new file mode 100644 index 000000000..ee3f6a56d --- /dev/null +++ b/UnitTests/RadioGroupTests.cs @@ -0,0 +1,116 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class RadioGroupTests { + [Fact] + public void Constructors_Defaults () + { + var rg = new RadioGroup (); + Assert.True (rg.CanFocus); + Assert.Empty (rg.RadioLabels); + Assert.Equal (0, rg.X); + Assert.Equal (0, rg.Y); + Assert.Equal (0, rg.Width); + Assert.Equal (0, rg.Height); + Assert.Equal (0, rg.SelectedItem); + + rg = new RadioGroup (new NStack.ustring [] { "Test" }); + Assert.True (rg.CanFocus); + Assert.Single (rg.RadioLabels); + Assert.Equal (0, rg.X); + Assert.Equal (0, rg.Y); + Assert.Equal (7, rg.Width); + Assert.Equal (1, rg.Height); + Assert.Equal (0, rg.SelectedItem); + + rg = new RadioGroup (new Rect (1, 2, 20, 5), new NStack.ustring [] { "Test" }); + Assert.True (rg.CanFocus); + Assert.Single (rg.RadioLabels); + Assert.Equal (1, rg.X); + Assert.Equal (2, rg.Y); + Assert.Equal (20, rg.Width); + Assert.Equal (5, rg.Height); + Assert.Equal (0, rg.SelectedItem); + + rg = new RadioGroup (1, 2, new NStack.ustring [] { "Test" }); + Assert.True (rg.CanFocus); + Assert.Single (rg.RadioLabels); + Assert.Equal (1, rg.X); + Assert.Equal (2, rg.Y); + Assert.Equal (7, rg.Width); + Assert.Equal (1, rg.Height); + Assert.Equal (0, rg.SelectedItem); + } + + [Fact] + public void Initialize_SelectedItem_With_Minus_One () + { + var rg = new RadioGroup (new NStack.ustring [] { "Test" }, -1); + Assert.Equal (-1, rg.SelectedItem); + Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()))); + Assert.Equal (0, rg.SelectedItem); + } + + [Fact] + public void DisplayMode_Width_Height_HorizontalSpace () + { + var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" }); + Assert.Equal (DisplayModeLayout.Vertical, rg.DisplayMode); + Assert.Equal (2, rg.RadioLabels.Length); + Assert.Equal (0, rg.X); + Assert.Equal (0, rg.Y); + Assert.Equal (11, rg.Width); + Assert.Equal (2, rg.Height); + + rg.DisplayMode = DisplayModeLayout.Horizontal; + Assert.Equal (DisplayModeLayout.Horizontal, rg.DisplayMode); + Assert.Equal (2, rg.HorizontalSpace); + Assert.Equal (0, rg.X); + Assert.Equal (0, rg.Y); + Assert.Equal (16, rg.Width); + Assert.Equal (1, rg.Height); + + rg.HorizontalSpace = 4; + Assert.Equal (DisplayModeLayout.Horizontal, rg.DisplayMode); + Assert.Equal (4, rg.HorizontalSpace); + Assert.Equal (0, rg.X); + Assert.Equal (0, rg.Y); + Assert.Equal (20, rg.Width); + Assert.Equal (1, rg.Height); + } + + [Fact] + public void SelectedItemChanged_Event () + { + var previousSelectedItem = -1; + var selectedItem = -1; + var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" }); + rg.SelectedItemChanged += (e) => { + previousSelectedItem = e.PreviousSelectedItem; + selectedItem = e.SelectedItem; + }; + + rg.SelectedItem = 1; + Assert.Equal (0, previousSelectedItem); + Assert.Equal (selectedItem, rg.SelectedItem); + } + + [Fact] + public void KeyBindings_Command () + { + var rg = new RadioGroup (new NStack.ustring [] { "Test", "New Test" }); + + Assert.True (rg.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.True (rg.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.True (rg.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.True (rg.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.True (rg.ProcessKey (new KeyEvent (Key.Space, new KeyModifiers ()))); + Assert.Equal (1, rg.SelectedItem); + } + } +} diff --git a/UnitTests/ScenarioTests.cs b/UnitTests/ScenarioTests.cs index e3a807e3f..d42dfdfa2 100644 --- a/UnitTests/ScenarioTests.cs +++ b/UnitTests/ScenarioTests.cs @@ -6,17 +6,21 @@ using System.Reflection; using Terminal.Gui; using UICatalog; using Xunit; +using Xunit.Abstractions; // Alias Console to MockConsole so we don't accidentally use Console using Console = Terminal.Gui.FakeConsole; namespace Terminal.Gui { public class ScenarioTests { - public ScenarioTests () + readonly ITestOutputHelper output; + + public ScenarioTests (ITestOutputHelper output) { #if DEBUG_IDISPOSABLE Responder.Instances.Clear (); #endif + this.output = output; } int CreateInput (string input) @@ -83,6 +87,10 @@ namespace Terminal.Gui { // Shutdown must be called to safely clean up Application if Init has been called Application.Shutdown (); + + if(abortCount != 0) { + output.WriteLine ($"Scenario {scenarioClass} had abort count of {abortCount}"); + } Assert.Equal (0, abortCount); // # of key up events should match # of iterations diff --git a/UnitTests/ScrollViewTests.cs b/UnitTests/ScrollViewTests.cs new file mode 100644 index 000000000..16f727574 --- /dev/null +++ b/UnitTests/ScrollViewTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class ScrollViewTests { + [Fact] + public void Constructors_Defaults () + { + var sv = new ScrollView (); + Assert.True (sv.CanFocus); + Assert.Equal (new Rect (0, 0, 0, 0), sv.Frame); + Assert.Equal (Rect.Empty, sv.Frame); + Assert.Equal (0, sv.X); + Assert.Equal (0, sv.Y); + Assert.Equal (0, sv.Width); + Assert.Equal (0, sv.Height); + Assert.Equal (Point.Empty, sv.ContentOffset); + Assert.Equal (Size.Empty, sv.ContentSize); + Assert.True (sv.AutoHideScrollBars); + Assert.True (sv.KeepContentAlwaysInViewport); + + sv = new ScrollView (new Rect (1, 2, 20, 10)); + Assert.True (sv.CanFocus); + Assert.Equal (new Rect (1, 2, 20, 10), sv.Frame); + Assert.Equal (1, sv.X); + Assert.Equal (2, sv.Y); + Assert.Equal (20, sv.Width); + Assert.Equal (10, sv.Height); + Assert.Equal (Point.Empty, sv.ContentOffset); + Assert.Equal (Size.Empty, sv.ContentSize); + Assert.True (sv.AutoHideScrollBars); + Assert.True (sv.KeepContentAlwaysInViewport); + } + + [Fact] + public void Adding_Views () + { + var sv = new ScrollView (new Rect (0, 0, 20, 10)) { + ContentSize = new Size (30, 20) + }; + sv.Add (new View () { Width = 10, Height = 5 }, + new View () { X = 12, Y = 7, Width = 10, Height = 5 }); + + Assert.Equal (new Size (30, 20), sv.ContentSize); + Assert.Equal (2, sv.Subviews [0].Subviews.Count); + } + + [Fact] + public void KeyBindings_Command () + { + var sv = new ScrollView (new Rect (0, 0, 20, 10)) { + ContentSize = new Size (40, 20) + }; + sv.Add (new View () { Width = 20, Height = 5 }, + new View () { X = 22, Y = 7, Width = 10, Height = 5 }); + + Assert.True (sv.KeepContentAlwaysInViewport); + Assert.True (sv.AutoHideScrollBars); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -1), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent ((Key)'v' | Key.AltMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (-1, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (-20, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (-20, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (-20, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + + sv.KeepContentAlwaysInViewport = false; + Assert.False (sv.KeepContentAlwaysInViewport); + Assert.True (sv.AutoHideScrollBars); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -1), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -10), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent ((Key)'v' | Key.AltMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -9), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (-1, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-20, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-39, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.PageDown | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-39, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (-39, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.PageUp | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-19, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (-19, 0), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (-19, 0), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (new Point (-19, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (new Point (-19, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, -19), sv.ContentOffset); + Assert.True (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-39, -19), sv.ContentOffset); + Assert.False (sv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (-39, -19), sv.ContentOffset); + } + } +} \ No newline at end of file diff --git a/UnitTests/TableViewTests.cs b/UnitTests/TableViewTests.cs index b16a77696..31d262e53 100644 --- a/UnitTests/TableViewTests.cs +++ b/UnitTests/TableViewTests.cs @@ -231,15 +231,10 @@ namespace Terminal.Gui.Views { Assert.False (tableView.IsSelected (2, 2)); } + [AutoInitShutdown] [Fact] public void PageDown_ExcludesHeaders () { - - var driver = new FakeDriver (); - Application.Init (driver, new FakeMainLoop (() => FakeConsole.ReadKey (true))); - driver.Init (() => { }); - - var tableView = new TableView () { Table = BuildTable (25, 50), MultiSelect = true, @@ -251,6 +246,11 @@ namespace Terminal.Gui.Views { tableView.Style.ShowHorizontalHeaderUnderline = true; tableView.Style.AlwaysShowHeaders = false; + // ensure that TableView has the input focus + Application.Top.Add (tableView); + Application.Top.FocusFirst (); + Assert.True (tableView.HasFocus); + Assert.Equal (0, tableView.RowOffset); tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())); @@ -262,9 +262,6 @@ namespace Terminal.Gui.Views { tableView.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ())); Assert.Equal (8, tableView.RowOffset); - - // Shutdown must be called to safely clean up Application if Init has been called - Application.Shutdown (); } [Fact] diff --git a/UnitTests/TextFieldTests.cs b/UnitTests/TextFieldTests.cs index 54c20cdfc..4a181382e 100644 --- a/UnitTests/TextFieldTests.cs +++ b/UnitTests/TextFieldTests.cs @@ -719,18 +719,22 @@ namespace Terminal.Gui.Views { var tf = new TextField ("ABC"); tf.EnsureFocus (); Assert.Equal ("ABC", tf.Text); + Assert.Equal (3, tf.CursorPosition); // now delete the C tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())); Assert.Equal ("AB", tf.Text); + Assert.Equal (2, tf.CursorPosition); // then delete the B tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())); Assert.Equal ("A", tf.Text); + Assert.Equal (1, tf.CursorPosition); // then delete the A tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ())); Assert.Equal ("", tf.Text); + Assert.Equal (0, tf.CursorPosition); } [Fact] diff --git a/UnitTests/TextViewTests.cs b/UnitTests/TextViewTests.cs index 031b137f6..89307ecec 100644 --- a/UnitTests/TextViewTests.cs +++ b/UnitTests/TextViewTests.cs @@ -1,5 +1,8 @@ using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; using Xunit; using Xunit.Abstractions; @@ -937,19 +940,21 @@ namespace Terminal.Gui.Views { bool iterationsFinished = false; while (!iterationsFinished) { - _textView.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())); switch (iteration) { case 0: + _textView.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ())); Assert.Equal (0, _textView.CursorPosition.X); Assert.Equal (0, _textView.CursorPosition.Y); Assert.Equal ($"{System.Environment.NewLine}This is the second line.", _textView.Text); break; case 1: + _textView.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (0, _textView.CursorPosition.X); Assert.Equal (0, _textView.CursorPosition.Y); Assert.Equal ("This is the second line.", _textView.Text); break; case 2: + _textView.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ())); Assert.Equal (0, _textView.CursorPosition.X); Assert.Equal (0, _textView.CursorPosition.Y); Assert.Equal ("", _textView.Text); @@ -974,19 +979,21 @@ namespace Terminal.Gui.Views { bool iterationsFinished = false; while (!iterationsFinished) { - _textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ())); switch (iteration) { case 0: + _textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ())); Assert.Equal (0, _textView.CursorPosition.X); Assert.Equal (1, _textView.CursorPosition.Y); Assert.Equal ($"This is the first line.{System.Environment.NewLine}", _textView.Text); break; case 1: + _textView.ProcessKey (new KeyEvent (Key.Backspace | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (23, _textView.CursorPosition.X); Assert.Equal (0, _textView.CursorPosition.Y); Assert.Equal ("This is the first line.", _textView.Text); break; case 2: + _textView.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ())); Assert.Equal (0, _textView.CursorPosition.X); Assert.Equal (0, _textView.CursorPosition.Y); Assert.Equal ("", _textView.Text); @@ -1439,7 +1446,7 @@ namespace Terminal.Gui.Views { Assert.True (_textView.Multiline); _textView.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ())); Assert.Equal ("\tTAB to jump between text fields.", _textView.Text); - _textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ())); + _textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())); Assert.Equal ("TAB to jump between text fields.", _textView.Text); } @@ -1483,7 +1490,7 @@ namespace Terminal.Gui.Views { [Fact] [InitShutdown] - public void Multiline_Setting_Changes_AllowsReturn_And_AllowsTab_And_Height () + public void Multiline_Setting_Changes_AllowsReturn_AllowsTab_Height_WordWrap () { Assert.True (_textView.Multiline); Assert.True (_textView.AllowsReturn); @@ -1491,7 +1498,10 @@ namespace Terminal.Gui.Views { Assert.True (_textView.AllowsTab); Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ()); Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ()); + Assert.False (_textView.WordWrap); + _textView.WordWrap = true; + Assert.True (_textView.WordWrap); _textView.Multiline = false; Assert.False (_textView.Multiline); Assert.False (_textView.AllowsReturn); @@ -1499,7 +1509,10 @@ namespace Terminal.Gui.Views { Assert.False (_textView.AllowsTab); Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ()); Assert.Equal ("Dim.Absolute(1)", _textView.Height.ToString ()); + Assert.False (_textView.WordWrap); + _textView.WordWrap = true; + Assert.False (_textView.WordWrap); _textView.Multiline = true; Assert.True (_textView.Multiline); Assert.True (_textView.AllowsReturn); @@ -1507,6 +1520,7 @@ namespace Terminal.Gui.Views { Assert.True (_textView.AllowsTab); Assert.Equal ("Dim.Absolute(30)", _textView.Width.ToString ()); Assert.Equal ("Dim.Absolute(10)", _textView.Height.ToString ()); + Assert.False (_textView.WordWrap); } [Fact] @@ -1532,7 +1546,7 @@ namespace Terminal.Gui.Views { } while (col > 0) { col--; - _textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ())); + _textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (new Point (col, 0), _textView.CursorPosition); leftCol = GetLeftCol (leftCol); Assert.Equal (leftCol, _textView.LeftColumn); @@ -1568,7 +1582,7 @@ namespace Terminal.Gui.Views { Assert.Equal (leftCol, _textView.LeftColumn); while (col > 0) { col--; - _textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ())); + _textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (new Point (col, 0), _textView.CursorPosition); leftCol = GetLeftCol (leftCol); Assert.Equal (leftCol, _textView.LeftColumn); @@ -1654,7 +1668,7 @@ namespace Terminal.Gui.Views { } while (col > 0) { col--; - _textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ())); + _textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (new Point (col, 0), _textView.CursorPosition); leftCol = GetLeftCol (leftCol); Assert.Equal (leftCol, _textView.LeftColumn); @@ -1710,7 +1724,7 @@ namespace Terminal.Gui.Views { leftCol = GetLeftCol (leftCol); while (col > 0) { col--; - _textView.ProcessKey (new KeyEvent (Key.BackTab, new KeyModifiers ())); + _textView.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ())); Assert.Equal (new Point (col, 0), _textView.CursorPosition); leftCol = GetLeftCol (leftCol); Assert.Equal (leftCol, _textView.LeftColumn); @@ -2042,7 +2056,7 @@ line. var tv = new TextView () { Width = 10, Height = 10, BottomOffset = 1 }; tv.Text = text; - tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End, null)); + tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End, new KeyModifiers ())); Assert.Equal (4, tv.TopRow); Assert.Equal (1, tv.BottomOffset); @@ -2072,7 +2086,7 @@ line. var tv = new TextView () { Width = 10, Height = 10, RightOffset = 1 }; tv.Text = text; - tv.ProcessKey (new KeyEvent (Key.End, null)); + tv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ())); Assert.Equal (4, tv.LeftColumn); Assert.Equal (1, tv.RightOffset); @@ -2283,5 +2297,443 @@ line. Assert.Equal (new Point (10, 0), tv.CursorPosition); Assert.Equal (1, tv.LeftColumn); } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var text = "This is the first line.\nThis is the second line.\nThis is the third line."; + var tv = new TextView () { + Width = 10, + Height = 2, + Text = text + }; + var top = Application.Top; + top.Add (tv); + Application.Begin (top); + + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text); + Assert.Equal (3, tv.Lines); + Assert.Equal (Point.Empty, tv.CursorPosition); + Assert.False (tv.ReadOnly); + Assert.True (tv.CanFocus); + + tv.CanFocus = false; + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + tv.CanFocus = true; + Assert.False (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (1, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (2, tv.CurrentRow); + Assert.Equal (23, tv.CurrentColumn); + Assert.Equal (tv.CurrentColumn, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 2), tv.CursorPosition); + Assert.False (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.NotNull (tv.Autocomplete); + Assert.Empty (tv.Autocomplete.AllSuggestions); + Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ()))); + tv.Redraw (tv.Bounds); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text); + Assert.Equal (new Point (24, 2), tv.CursorPosition); + Assert.Empty (tv.Autocomplete.Suggestions); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()))); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.", tv.Text); + Assert.Equal (new Point (23, 2), tv.CursorPosition); + tv.Autocomplete.AllSuggestions = Regex.Matches (tv.Text.ToString (), "\\w+") + .Select (s => s.Value) + .Distinct ().ToList (); + Assert.Equal (7, tv.Autocomplete.AllSuggestions.Count); + Assert.Equal ("This", tv.Autocomplete.AllSuggestions [0]); + Assert.Equal ("is", tv.Autocomplete.AllSuggestions [1]); + Assert.Equal ("the", tv.Autocomplete.AllSuggestions [2]); + Assert.Equal ("first", tv.Autocomplete.AllSuggestions [3]); + Assert.Equal ("line", tv.Autocomplete.AllSuggestions [4]); + Assert.Equal ("second", tv.Autocomplete.AllSuggestions [5]); + Assert.Equal ("third", tv.Autocomplete.AllSuggestions [^1]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.F, new KeyModifiers ()))); + tv.Redraw (tv.Bounds); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.F", tv.Text); + Assert.Equal (new Point (24, 2), tv.CursorPosition); + Assert.Single (tv.Autocomplete.Suggestions); + Assert.Equal ("first", tv.Autocomplete.Suggestions [0]); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (28, 2), tv.CursorPosition); + Assert.Single (tv.Autocomplete.Suggestions); + Assert.Equal ("first", tv.Autocomplete.Suggestions [0]); + tv.Autocomplete.AllSuggestions = new List (); + tv.Autocomplete.ClearSuggestions (); + Assert.Empty (tv.Autocomplete.AllSuggestions); + Assert.Empty (tv.Autocomplete.Suggestions); + Assert.True (tv.ProcessKey (new KeyEvent (Key.PageUp, new KeyModifiers ()))); + Assert.Equal (24, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (24, 1), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (((int)'V' + Key.AltMask), new KeyModifiers ()))); + Assert.Equal (23, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.PageDown, new KeyModifiers ()))); + Assert.Equal (24, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 1), tv.CursorPosition); // gets the previous length + Assert.True (tv.ProcessKey (new KeyEvent (Key.V | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (28, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 2), tv.CursorPosition); // gets the previous length + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.PageUp | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (24, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 1), tv.CursorPosition); // gets the previous length + Assert.Equal (25, tv.SelectedLength); + Assert.Equal (".\nThis is the third line.", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.PageDown | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (28, tv.GetCurrentLine ().Count); + Assert.Equal (new Point (23, 2), tv.CursorPosition); // gets the previous length + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (Point.Empty, tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.N | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 1), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.P | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (new Point (0, 1), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 1), tv.CursorPosition); + Assert.Equal (24, tv.SelectedLength); + Assert.Equal ("This is the first line.\n", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorUp | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (1, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (new Point (1, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorRight | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (1, 0), tv.CursorPosition); + Assert.Equal (1, tv.SelectedLength); + Assert.Equal ("T", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CursorLeft | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ()))); + Assert.Equal ($"This is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ()))); + Assert.Equal ($"his is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal ($"is is the first line.{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (21, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ()))); + Assert.Equal ($"is is the first line{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (20, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (19, tv.SelectedLength); + Assert.Equal ("is is the first lin", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.K | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal ("is is the first lin", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal ("is is the first lin", Clipboard.Contents); + tv.CursorPosition = Point.Empty; + Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal ("is is the first lin", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal ("is is the first lin", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.K | Key.AltMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + tv.ReadOnly = true; + Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + tv.ReadOnly = false; + Assert.True (tv.ProcessKey (new KeyEvent (Key.Y | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Space | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.Equal (19, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Space | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal (19, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + tv.SelectionStartColumn = 0; + Assert.True (tv.ProcessKey (new KeyEvent (((int)'C' + Key.AltMask), new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (19, tv.SelectedLength); + Assert.Equal ("is is the first lin", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.True (tv.ProcessKey (new KeyEvent (Key.C | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"is is the first lin{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (19, 0), tv.CursorPosition); + Assert.Equal (19, tv.SelectedLength); + Assert.Equal ("is is the first lin", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.True (tv.ProcessKey (new KeyEvent (((int)'W' + Key.AltMask), new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.Equal ("is is the first lin", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.W | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.Equal ("", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.X | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.Equal (0, tv.SelectionStartColumn); + Assert.Equal (0, tv.SelectionStartRow); + Assert.Equal ("", Clipboard.Contents); + Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (28, 2), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (18, 2), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorLeft | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (12, 2), tv.CursorPosition); + Assert.Equal (6, tv.SelectedLength); + Assert.Equal ("third ", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent ((Key)((int)'B' + Key.AltMask), new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (8, 2), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorRight, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (12, 2), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.CursorRight | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (18, 2), tv.CursorPosition); + Assert.Equal (6, tv.SelectedLength); + Assert.Equal ("third ", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent ((Key)((int)'F' + Key.AltMask), new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (28, 2), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Home | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.DeleteChar | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); Assert.True (tv.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third line.first", tv.Text); + Assert.Equal (new Point (28, 1), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.False (tv.Selecting); Assert.True (tv.ProcessKey (new KeyEvent (Key.Backspace | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (18, 1), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.AllowsReturn); + tv.AllowsReturn = false; + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.False (tv.Selecting); + Assert.False (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal ($"This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.False (tv.AllowsReturn); + tv.AllowsReturn = true; + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.True (tv.ProcessKey (new KeyEvent (Key.Enter, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (0, 1), tv.CursorPosition); + Assert.Equal (0, tv.SelectedLength); + Assert.Equal ("", tv.SelectedText); + Assert.False (tv.Selecting); + Assert.True (tv.AllowsReturn); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.End | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (18, 2), tv.CursorPosition); + Assert.Equal (43, tv.SelectedLength); + Assert.Equal ("This is the second line.\nThis is the third ", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.CtrlMask | Key.Home | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (0, 0), tv.CursorPosition); + Assert.Equal (1, tv.SelectedLength); + Assert.Equal ("\n", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.ProcessKey (new KeyEvent (Key.T | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.Equal (new Point (18, 2), tv.CursorPosition); + Assert.Equal (44, tv.SelectedLength); + Assert.Equal ("\nThis is the second line.\nThis is the third ", tv.SelectedText); + Assert.True (tv.Selecting); + Assert.True (tv.Used); + Assert.True (tv.ProcessKey (new KeyEvent (Key.InsertChar, new KeyModifiers ()))); + Assert.False (tv.Used); + Assert.True (tv.AllowsTab); + Assert.Equal (new Point (18, 2), tv.CursorPosition); + tv.AllowsTab = false; + Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.False (tv.AllowsTab); + tv.AllowsTab = true; + Assert.Equal (new Point (18, 2), tv.CursorPosition); + Assert.True (tv.Selecting); + tv.Selecting = false; + Assert.True (tv.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t", tv.Text); + Assert.True (tv.AllowsTab); + tv.AllowsTab = false; + Assert.False (tv.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third \t", tv.Text); + Assert.False (tv.AllowsTab); + tv.AllowsTab = true; + Assert.True (tv.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"{Environment.NewLine}This is the second line.{Environment.NewLine}This is the third ", tv.Text); + Assert.True (tv.AllowsTab); + Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ()))); + Assert.False (tv.ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ()))); + Assert.False (tv.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()))); + Assert.False (tv.ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ()))); + } } -} +} \ No newline at end of file diff --git a/UnitTests/TimeFieldTests.cs b/UnitTests/TimeFieldTests.cs new file mode 100644 index 000000000..f9d08c373 --- /dev/null +++ b/UnitTests/TimeFieldTests.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Terminal.Gui.Views { + public class TimeFieldTests { + [Fact] + public void Constructors_Defaults () + { + var tf = new TimeField (); + Assert.False (tf.IsShortFormat); + Assert.Equal (TimeSpan.MinValue, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); + + var time = DateTime.Now.TimeOfDay; + tf = new TimeField (time); + Assert.False (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame); + + tf = new TimeField (1, 2, time); + Assert.False (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame); + + tf = new TimeField (3, 4, time, true); + Assert.True (tf.IsShortFormat); + Assert.Equal (time, tf.Time); + Assert.Equal (1, tf.CursorPosition); + Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame); + + tf.IsShortFormat = false; + Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame); + Assert.Equal (10, tf.Width); + } + + [Fact] + public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format () + { + var tf = new TimeField (); + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 0; + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 9; + Assert.Equal (8, tf.CursorPosition); + tf.IsShortFormat = true; + tf.CursorPosition = 0; + Assert.Equal (1, tf.CursorPosition); + tf.CursorPosition = 6; + Assert.Equal (5, tf.CursorPosition); + } + + [Fact] + public void KeyBindings_Command () + { + TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19")); + tf.ReadOnly = true; + Assert.True (tf.ProcessKey (new KeyEvent (Key.DeleteChar, new KeyModifiers ()))); + Assert.Equal (" 12:12:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.ProcessKey (new KeyEvent (Key.D | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (" 02:12:19", tf.Text); + tf.CursorPosition = 4; + tf.ReadOnly = true; + Assert.True (tf.ProcessKey (new KeyEvent (Key.Delete, new KeyModifiers ()))); + Assert.Equal (" 02:12:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.ProcessKey (new KeyEvent (Key.Backspace, new KeyModifiers ()))); + Assert.Equal (" 02:02:19", tf.Text); + Assert.True (tf.ProcessKey (new KeyEvent (Key.Home, new KeyModifiers ()))); + Assert.Equal (1, tf.CursorPosition); + Assert.True (tf.ProcessKey (new KeyEvent (Key.End, new KeyModifiers ()))); + Assert.Equal (8, tf.CursorPosition); + Assert.True (tf.ProcessKey (new KeyEvent (Key.A | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (1, tf.CursorPosition); + Assert.True (tf.ProcessKey (new KeyEvent (Key.E | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (8, tf.CursorPosition); + Assert.True (tf.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (7, tf.CursorPosition); + Assert.True (tf.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (8, tf.CursorPosition); + Assert.False (tf.ProcessKey (new KeyEvent (Key.A, new KeyModifiers ()))); + tf.ReadOnly = true; + tf.CursorPosition = 1; + Assert.True (tf.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Equal (" 02:02:19", tf.Text); + tf.ReadOnly = false; + Assert.True (tf.ProcessKey (new KeyEvent (Key.D1, new KeyModifiers ()))); + Assert.Equal (" 12:02:19", tf.Text); + } + } +} diff --git a/UnitTests/ToplevelTests.cs b/UnitTests/ToplevelTests.cs index 3bb59ba3b..1466e579b 100644 --- a/UnitTests/ToplevelTests.cs +++ b/UnitTests/ToplevelTests.cs @@ -316,5 +316,336 @@ namespace Terminal.Gui.Core { win.MouseEvent (new MouseEvent () { X = 6, Y = 0, Flags = MouseFlags.Button1Pressed }); Assert.Null (Toplevel.dragPosition); } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command () + { + var isRunning = false; + + var win1 = new Window ("Win1") { Width = Dim.Percent (50f), Height = Dim.Fill () }; + var lblTf1W1 = new Label ("Enter text in TextField on Win1:"); + var tf1W1 = new TextField ("Text1 on Win1") { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () }; + var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Y = Pos.Bottom (lblTf1W1) + 1 }; + var tvW1 = new TextView () { X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" }; + var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Y = Pos.Bottom (lblTvW1) + 1 }; + var tf2W1 = new TextField ("Text2 on Win1") { X = Pos.Left (tf1W1), Width = Dim.Fill () }; + win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1); + + var win2 = new Window ("Win2") { X = Pos.Right (win1) + 1, Width = Dim.Percent (50f), Height = Dim.Fill () }; + var lblTf1W2 = new Label ("Enter text in TextField on Win2:"); + var tf1W2 = new TextField ("Text1 on Win2") { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () }; + var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Y = Pos.Bottom (lblTf1W2) + 1 }; + var tvW2 = new TextView () { X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" }; + var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Y = Pos.Bottom (lblTvW2) + 1 }; + var tf2W2 = new TextField ("Text2 on Win2") { X = Pos.Left (tf1W2), Width = Dim.Fill () }; + win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2); + + var top = Application.Top; + top.Add (win1, win2); + top.Loaded += () => isRunning = true; + top.Closing += (_) => isRunning = false; + Application.Begin (top); + top.Running = true; + + Assert.Equal (new Rect (0, 0, 40, 25), win1.Frame); + Assert.Equal (new Rect (41, 0, 40, 25), win2.Frame); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf1W1, top.MostFocused); + + Assert.True (isRunning); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.QuitKey, new KeyModifiers ()))); + Assert.False (isRunning); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ()))); + Assert.False (top.Focused.ProcessKey (new KeyEvent (Key.F5, new KeyModifiers ()))); + + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf1W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf1W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.I | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf1W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ()))); + Assert.Equal (win2, top.Focused); + Assert.Equal (tf1W2, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf1W1, top.MostFocused); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.Equal (new Point (0, 0), tvW1.CursorPosition); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tvW1, top.MostFocused); + Assert.Equal (new Point (16, 1), tvW1.CursorPosition); + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, top.Focused); + Assert.Equal (tf2W1, top.MostFocused); + + Assert.True (top.Focused.ProcessKey (new KeyEvent (Key.L | Key.CtrlMask, new KeyModifiers ()))); + } + + [Fact] + [AutoInitShutdown] + public void KeyBindings_Command_With_MdiTop () + { + var top = Application.Top; + Assert.Null (Application.MdiTop); + top.IsMdiContainer = true; + Assert.Equal (Application.Top, Application.MdiTop); + + var isRunning = true; + + var win1 = new Window ("Win1") { Width = Dim.Percent (50f), Height = Dim.Fill () }; + var lblTf1W1 = new Label ("Enter text in TextField on Win1:"); + var tf1W1 = new TextField ("Text1 on Win1") { X = Pos.Right (lblTf1W1) + 1, Width = Dim.Fill () }; + var lblTvW1 = new Label ("Enter text in TextView on Win1:") { Y = Pos.Bottom (lblTf1W1) + 1 }; + var tvW1 = new TextView () { X = Pos.Left (tf1W1), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win1" }; + var lblTf2W1 = new Label ("Enter text in TextField on Win1:") { Y = Pos.Bottom (lblTvW1) + 1 }; + var tf2W1 = new TextField ("Text2 on Win1") { X = Pos.Left (tf1W1), Width = Dim.Fill () }; + win1.Add (lblTf1W1, tf1W1, lblTvW1, tvW1, lblTf2W1, tf2W1); + + var win2 = new Window ("Win2") { Width = Dim.Percent (50f), Height = Dim.Fill () }; + var lblTf1W2 = new Label ("Enter text in TextField on Win2:"); + var tf1W2 = new TextField ("Text1 on Win2") { X = Pos.Right (lblTf1W2) + 1, Width = Dim.Fill () }; + var lblTvW2 = new Label ("Enter text in TextView on Win2:") { Y = Pos.Bottom (lblTf1W2) + 1 }; + var tvW2 = new TextView () { X = Pos.Left (tf1W2), Width = Dim.Fill (), Height = 2, Text = "First line Win1\nSecond line Win2" }; + var lblTf2W2 = new Label ("Enter text in TextField on Win2:") { Y = Pos.Bottom (lblTvW2) + 1 }; + var tf2W2 = new TextField ("Text2 on Win2") { X = Pos.Left (tf1W2), Width = Dim.Fill () }; + win2.Add (lblTf1W2, tf1W2, lblTvW2, tvW2, lblTf2W2, tf2W2); + + win1.Closing += (_) => isRunning = false; + Assert.Null (top.Focused); + Assert.Equal (top, Application.Current); + Assert.True (top.IsCurrentTop); + Assert.Equal (top, Application.MdiTop); + Application.Begin (win1); + Assert.Equal (new Rect (0, 0, 40, 25), win1.Frame); + Assert.NotEqual (top, Application.Current); + Assert.False (top.IsCurrentTop); + Assert.Equal (win1, Application.Current); + Assert.True (win1.IsCurrentTop); + Assert.True (win1.IsMdiChild); + Assert.Null (top.Focused); + Assert.Null (top.MostFocused); + Assert.Equal (win1.Subviews [0], win1.Focused); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.Single (Application.MdiChildes); + Application.Begin (win2); + Assert.Equal (new Rect (0, 0, 40, 25), win2.Frame); + Assert.NotEqual (top, Application.Current); + Assert.False (top.IsCurrentTop); + Assert.Equal (win2, Application.Current); + Assert.True (win2.IsCurrentTop); + Assert.True (win2.IsMdiChild); + Assert.Null (top.Focused); + Assert.Null (top.MostFocused); + Assert.Equal (win2.Subviews [0], win2.Focused); + Assert.Equal (tf1W2, win2.MostFocused); + Assert.Equal (2, Application.MdiChildes.Count); + + Application.ShowChild (win1); + Assert.Equal (win1, Application.Current); + Assert.Equal (win1, Application.MdiChildes [0]); + win1.Running = true; + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.QuitKey, new KeyModifiers ()))); + Assert.False (isRunning); + Assert.False (win1.Running); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Z | Key.CtrlMask, new KeyModifiers ()))); + Assert.False (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.F5, new KeyModifiers ()))); + + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.True (win1.IsCurrentTop); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal ($"\tFirst line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal ($"First line Win1{Environment.NewLine}Second line Win1", tvW1.Text); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorRight, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.I | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.BackTab | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorLeft, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorUp, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf2W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win2, Application.MdiChildes [0]); + Assert.Equal (tf1W2, win2.MostFocused); + tf2W2.SetFocus (); + Assert.True (tf2W2.HasFocus); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.Tab | Key.CtrlMask | Key.ShiftMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.AlternateForwardKey, new KeyModifiers ()))); + Assert.Equal (win2, Application.MdiChildes [0]); + Assert.Equal (tf2W2, win2.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Application.AlternateBackwardKey, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.B | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf1W1, win1.MostFocused); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.CursorDown, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.Equal (new Point (0, 0), tvW1.CursorPosition); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.End | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tvW1, win1.MostFocused); + Assert.Equal (new Point (16, 1), tvW1.CursorPosition); + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.F | Key.CtrlMask, new KeyModifiers ()))); + Assert.Equal (win1, Application.MdiChildes [0]); + Assert.Equal (tf2W1, win1.MostFocused); + + Assert.True (Application.MdiChildes [0].ProcessKey (new KeyEvent (Key.L | Key.CtrlMask, new KeyModifiers ()))); + } + + [Fact] + public void Added_Event_Should_Not_Be_Used_To_Initialize_Toplevel_Events () + { + Key alternateForwardKey = default; + Key alternateBackwardKey = default; + Key quitKey = default; + var wasAdded = false; + + var view = new View (); + view.Added += View_Added; + + void View_Added (View obj) + { + Assert.Throws (() => Application.Top.AlternateForwardKeyChanged += (e) => alternateForwardKey = e); + Assert.Throws (() => Application.Top.AlternateBackwardKeyChanged += (e) => alternateBackwardKey = e); + Assert.Throws (() => Application.Top.QuitKeyChanged += (e) => quitKey = e); + Assert.False (wasAdded); + wasAdded = true; + view.Added -= View_Added; + } + + var win = new Window (); + win.Add (view); + Application.Init (new FakeDriver (), new FakeMainLoop (() => FakeConsole.ReadKey (true))); + var top = Application.Top; + top.Add (win); + + Assert.True (wasAdded); + + Application.Shutdown (); + } + + [Fact] + [AutoInitShutdown] + public void AlternateForwardKeyChanged_AlternateBackwardKeyChanged_QuitKeyChanged_Events () + { + Key alternateForwardKey = default; + Key alternateBackwardKey = default; + Key quitKey = default; + + var view = new View (); + view.Initialized += View_Initialized; + + void View_Initialized (object sender, EventArgs e) + { + Application.Top.AlternateForwardKeyChanged += (e) => alternateForwardKey = e; + Application.Top.AlternateBackwardKeyChanged += (e) => alternateBackwardKey = e; + Application.Top.QuitKeyChanged += (e) => quitKey = e; + } + + var win = new Window (); + win.Add (view); + var top = Application.Top; + top.Add (win); + Application.Begin (top); + + Assert.Equal (Key.Null, alternateForwardKey); + Assert.Equal (Key.Null, alternateBackwardKey); + Assert.Equal (Key.Null, quitKey); + + Assert.Equal (Key.PageDown | Key.CtrlMask, Application.AlternateForwardKey); + Assert.Equal (Key.PageUp | Key.CtrlMask, Application.AlternateBackwardKey); + Assert.Equal (Key.Q | Key.CtrlMask, Application.QuitKey); + + Application.AlternateForwardKey = Key.A; + Application.AlternateBackwardKey = Key.B; + Application.QuitKey = Key.C; + + Assert.Equal (Key.PageDown | Key.CtrlMask, alternateForwardKey); + Assert.Equal (Key.PageUp | Key.CtrlMask, alternateBackwardKey); + Assert.Equal (Key.Q | Key.CtrlMask, quitKey); + + Assert.Equal (Key.A, Application.AlternateForwardKey); + Assert.Equal (Key.B, Application.AlternateBackwardKey); + Assert.Equal (Key.C, Application.QuitKey); + + // 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); + } } -} +} \ No newline at end of file diff --git a/UnitTests/ViewTests.cs b/UnitTests/ViewTests.cs index 52baddb70..4dfc3da95 100644 --- a/UnitTests/ViewTests.cs +++ b/UnitTests/ViewTests.cs @@ -1574,5 +1574,30 @@ namespace Terminal.Gui.Views { return runesCount; } } + + [Fact] + public void GetTopSuperView_Test () + { + var v1 = new View (); + var fv1 = new FrameView (); + fv1.Add (v1); + var tf1 = new TextField (); + var w1 = new Window (); + w1.Add (fv1, tf1); + var top1 = new Toplevel (); + top1.Add (w1); + + var v2 = new View (); + var fv2 = new FrameView (); + fv2.Add (v2); + var tf2 = new TextField (); + var w2 = new Window (); + w2.Add (fv2, tf2); + var top2 = new Toplevel (); + top2.Add (w2); + + Assert.Equal (top1, v1.GetTopSuperView ()); + Assert.Equal (top2, v2.GetTopSuperView ()); + } } }