From 53183e514adbe696595d0c6b9ff8027973fe2dd3 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 29 Apr 2023 10:19:36 +0100 Subject: [PATCH 1/8] Refactor CollectionNavigator to a base and a collection implementation --- Terminal.Gui/Text/CollectionNavigator.cs | 212 ++----------------- Terminal.Gui/Text/CollectionNavigatorBase.cs | 207 ++++++++++++++++++ 2 files changed, 220 insertions(+), 199 deletions(-) create mode 100644 Terminal.Gui/Text/CollectionNavigatorBase.cs diff --git a/Terminal.Gui/Text/CollectionNavigator.cs b/Terminal.Gui/Text/CollectionNavigator.cs index 18eb9bb014..b218e66661 100644 --- a/Terminal.Gui/Text/CollectionNavigator.cs +++ b/Terminal.Gui/Text/CollectionNavigator.cs @@ -3,223 +3,37 @@ using System.Linq; namespace Terminal.Gui { - /// - /// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. - /// The is used to find the next item in the collection that matches the search string - /// when is called. - /// - /// If the user types keystrokes that can't be found in the collection, - /// the search string is cleared and the next item is found that starts with the last keystroke. - /// - /// - /// If the user pauses keystrokes for a short time (see ), the search string is cleared. - /// - /// - public partial class CollectionNavigator { - /// - /// Constructs a new CollectionNavigator. - /// - public CollectionNavigator () { } - - /// - /// Constructs a new CollectionNavigator for the given collection. - /// - /// - public CollectionNavigator (IEnumerable collection) => Collection = collection; - - DateTime lastKeystroke = DateTime.Now; - /// - /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is - /// reset on each call to . The default is 500ms. - /// - public int TypingDelay { get; set; } = 500; - - /// - /// The compararer function to use when searching the collection. - /// - public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + /// + /// This implementation is based on a static of objects. + public class CollectionNavigator : CollectionNavigatorBase + { /// /// The collection of objects to search. is used to search the collection. /// public IEnumerable Collection { get; set; } /// - /// This event is invoked when changes. Useful for debugging. - /// - public event EventHandler SearchStringChanged; - - private string _searchString = ""; - /// - /// Gets the current search string. This includes the set of keystrokes that have been pressed - /// since the last unsuccessful match or after ) milliseconds. Useful for debugging. - /// - public string SearchString { - get => _searchString; - private set { - _searchString = value; - OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); - } - } - - /// - /// Invoked when the changes. Useful for debugging. Invokes the event. - /// - /// - public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) - { - SearchStringChanged?.Invoke (this, e); - } - - /// - /// Gets the index of the next item in the collection that matches the current plus the provided character (typically - /// from a key press). + /// Constructs a new CollectionNavigator. /// - /// The index in the collection to start the search from. - /// The character of the key the user pressed. - /// The index of the item that matches what the user has typed. - /// Returns if no item in the collection matched. - public int GetNextMatchingItem (int currentIndex, char keyStruck) - { - AssertCollectionIsNotNull (); - if (!char.IsControl (keyStruck)) { - - // maybe user pressed 'd' and now presses 'd' again. - // a candidate search is things that begin with "dd" - // but if we find none then we must fallback on cycling - // d instead and discard the candidate state - string candidateState = ""; - - // is it a second or third (etc) keystroke within a short time - if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { - // "dd" is a candidate - candidateState = SearchString + keyStruck; - } else { - // its a fresh keystroke after some time - // or its first ever key press - SearchString = new string (keyStruck, 1); - } - - var idxCandidate = GetNextMatchingItem (currentIndex, candidateState, - // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" - candidateState.Length > 1); - - if (idxCandidate != -1) { - // found "dd" so candidate searchstring is accepted - lastKeystroke = DateTime.Now; - SearchString = candidateState; - return idxCandidate; - } - - //// nothing matches "dd" so discard it as a candidate - //// and just cycle "d" instead - lastKeystroke = DateTime.Now; - idxCandidate = GetNextMatchingItem (currentIndex, candidateState); - - // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' - // instead of "can" + 'd'). - if (SearchString.Length > 1 && idxCandidate == -1) { - // ignore it since we're still within the typing delay - // don't add it to SearchString either - return currentIndex; - } - - // if no changes to current state manifested - if (idxCandidate == currentIndex || idxCandidate == -1) { - // clear history and treat as a fresh letter - ClearSearchString (); - - // match on the fresh letter alone - SearchString = new string (keyStruck, 1); - idxCandidate = GetNextMatchingItem (currentIndex, SearchString); - return idxCandidate == -1 ? currentIndex : idxCandidate; - } - - // Found another "d" or just leave index as it was - return idxCandidate; - - } else { - // clear state because keypress was a control char - ClearSearchString (); - - // control char indicates no selection - return -1; - } - } + public CollectionNavigator () { } /// - /// Gets the index of the next item in the collection that matches . + /// Constructs a new CollectionNavigator for the given collection. /// - /// The index in the collection to start the search from. - /// The search string to use. - /// Set to to stop the search on the first match - /// if there are multiple matches for . - /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), - /// the next matching item will be returned, even if it is above in the collection. - /// - /// The index of the next matching item or if no match was found. - internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) - { - if (string.IsNullOrEmpty (search)) { - return -1; - } - AssertCollectionIsNotNull (); - - // find indexes of items that start with the search text - int [] matchingIndexes = Collection.Select ((item, idx) => (item, idx)) - .Where (k => k.item?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) - .Select (k => k.idx) - .ToArray (); - - // if there are items beginning with search - if (matchingIndexes.Length > 0) { - // is one of them currently selected? - var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); - - if (currentlySelected == -1) { - // we are not currently selecting any item beginning with the search - // so jump to first item in list that begins with the letter - return matchingIndexes [0]; - } else { - - // the current index is part of the matching collection - if (minimizeMovement) { - // if we would rather not jump around (e.g. user is typing lots of text to get this match) - return matchingIndexes [currentlySelected]; - } - - // cycle to next (circular) - return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; - } - } - - // nothing starts with the search - return -1; - } + /// + public CollectionNavigator (IEnumerable collection) => Collection = collection; - private void AssertCollectionIsNotNull () + /// + protected override IEnumerable> GetCollection () { if (Collection == null) { throw new InvalidOperationException ("Collection is null"); } - } - private void ClearSearchString () - { - SearchString = ""; - lastKeystroke = DateTime.Now; + return Collection.Select ((item, idx) => KeyValuePair.Create(idx, item)); } - /// - /// Returns true if is a searchable key - /// (e.g. letters, numbers, etc) that are valid to pass to this - /// class for search filtering. - /// - /// - /// - public static bool IsCompatibleKey (KeyEvent kb) - { - return !kb.IsAlt && !kb.IsCtrl; - } + } } diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs new file mode 100644 index 0000000000..5d15b4815b --- /dev/null +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Terminal.Gui { + /// + /// Navigates a collection of items using keystrokes. The keystrokes are used to build a search string. + /// The is used to find the next item in the collection that matches the search string + /// when is called. + /// + /// If the user types keystrokes that can't be found in the collection, + /// the search string is cleared and the next item is found that starts with the last keystroke. + /// + /// + /// If the user pauses keystrokes for a short time (see ), the search string is cleared. + /// + /// + public abstract class CollectionNavigatorBase { + + DateTime lastKeystroke = DateTime.Now; + /// + /// Gets or sets the number of milliseconds to delay before clearing the search string. The delay is + /// reset on each call to . The default is 500ms. + /// + public int TypingDelay { get; set; } = 500; + + /// + /// The compararer function to use when searching the collection. + /// + public StringComparer Comparer { get; set; } = StringComparer.InvariantCultureIgnoreCase; + + /// + /// This event is invoked when changes. Useful for debugging. + /// + public event EventHandler SearchStringChanged; + + private string _searchString = ""; + /// + /// Gets the current search string. This includes the set of keystrokes that have been pressed + /// since the last unsuccessful match or after ) milliseconds. Useful for debugging. + /// + public string SearchString { + get => _searchString; + private set { + _searchString = value; + OnSearchStringChanged (new KeystrokeNavigatorEventArgs (value)); + } + } + + /// + /// Invoked when the changes. Useful for debugging. Invokes the event. + /// + /// + public virtual void OnSearchStringChanged (KeystrokeNavigatorEventArgs e) + { + SearchStringChanged?.Invoke (this, e); + } + + /// + /// Gets the index of the next item in the collection that matches the current plus the provided character (typically + /// from a key press). + /// + /// The index in the collection to start the search from. + /// The character of the key the user pressed. + /// The index of the item that matches what the user has typed. + /// Returns if no item in the collection matched. + public int GetNextMatchingItem (int currentIndex, char keyStruck) + { + if (!char.IsControl (keyStruck)) { + + // maybe user pressed 'd' and now presses 'd' again. + // a candidate search is things that begin with "dd" + // but if we find none then we must fallback on cycling + // d instead and discard the candidate state + string candidateState = ""; + + // is it a second or third (etc) keystroke within a short time + if (SearchString.Length > 0 && DateTime.Now - lastKeystroke < TimeSpan.FromMilliseconds (TypingDelay)) { + // "dd" is a candidate + candidateState = SearchString + keyStruck; + } else { + // its a fresh keystroke after some time + // or its first ever key press + SearchString = new string (keyStruck, 1); + } + + var idxCandidate = GetNextMatchingItem (currentIndex, candidateState, + // prefer not to move if there are multiple characters e.g. "ca" + 'r' should stay on "car" and not jump to "cart" + candidateState.Length > 1); + + if (idxCandidate != -1) { + // found "dd" so candidate searchstring is accepted + lastKeystroke = DateTime.Now; + SearchString = candidateState; + return idxCandidate; + } + + //// nothing matches "dd" so discard it as a candidate + //// and just cycle "d" instead + lastKeystroke = DateTime.Now; + idxCandidate = GetNextMatchingItem (currentIndex, candidateState); + + // if a match wasn't found, the user typed a 'wrong' key in their search ("can" + 'z' + // instead of "can" + 'd'). + if (SearchString.Length > 1 && idxCandidate == -1) { + // ignore it since we're still within the typing delay + // don't add it to SearchString either + return currentIndex; + } + + // if no changes to current state manifested + if (idxCandidate == currentIndex || idxCandidate == -1) { + // clear history and treat as a fresh letter + ClearSearchString (); + + // match on the fresh letter alone + SearchString = new string (keyStruck, 1); + idxCandidate = GetNextMatchingItem (currentIndex, SearchString); + return idxCandidate == -1 ? currentIndex : idxCandidate; + } + + // Found another "d" or just leave index as it was + return idxCandidate; + + } else { + // clear state because keypress was a control char + ClearSearchString (); + + // control char indicates no selection + return -1; + } + } + + /// + /// Gets the index of the next item in the collection that matches . + /// + /// The index in the collection to start the search from. + /// The search string to use. + /// Set to to stop the search on the first match + /// if there are multiple matches for . + /// e.g. "ca" + 'r' should stay on "car" and not jump to "cart". If (the default), + /// the next matching item will be returned, even if it is above in the collection. + /// + /// The index of the next matching item or if no match was found. + internal int GetNextMatchingItem (int currentIndex, string search, bool minimizeMovement = false) + { + if (string.IsNullOrEmpty (search)) { + return -1; + } + + // find indexes of items that start with the search text + int [] matchingIndexes = GetCollection() + .Where (k => k.Value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) + .Select (k => k.Key) + .ToArray (); + + // if there are items beginning with search + if (matchingIndexes.Length > 0) { + // is one of them currently selected? + var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); + + if (currentlySelected == -1) { + // we are not currently selecting any item beginning with the search + // so jump to first item in list that begins with the letter + return matchingIndexes [0]; + } else { + + // the current index is part of the matching collection + if (minimizeMovement) { + // if we would rather not jump around (e.g. user is typing lots of text to get this match) + return matchingIndexes [currentlySelected]; + } + + // cycle to next (circular) + return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; + } + } + + // nothing starts with the search + return -1; + } + + /// + /// Returns the index and value of each item that is to be considered for searching. + /// + /// + protected abstract IEnumerable> GetCollection (); + + private void ClearSearchString () + { + SearchString = ""; + lastKeystroke = DateTime.Now; + } + + /// + /// Returns true if is a searchable key + /// (e.g. letters, numbers, etc) that are valid to pass to this + /// class for search filtering. + /// + /// + /// + public static bool IsCompatibleKey (KeyEvent kb) + { + return !kb.IsAlt && !kb.IsCtrl; + } + } +} From 5a7a0bf8cc831bb78c5b56fb4cb7cd4ec46a7510 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 29 Apr 2023 13:14:17 +0100 Subject: [PATCH 2/8] Refactor CollectionNavigatorBase to look for first match smartly --- Terminal.Gui/Text/CollectionNavigator.cs | 18 ++--- Terminal.Gui/Text/CollectionNavigatorBase.cs | 69 ++++++++++++-------- Terminal.Gui/Views/ListView.cs | 2 +- 3 files changed, 54 insertions(+), 35 deletions(-) diff --git a/Terminal.Gui/Text/CollectionNavigator.cs b/Terminal.Gui/Text/CollectionNavigator.cs index b218e66661..7a23934f4e 100644 --- a/Terminal.Gui/Text/CollectionNavigator.cs +++ b/Terminal.Gui/Text/CollectionNavigator.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; @@ -11,7 +12,7 @@ public class CollectionNavigator : CollectionNavigatorBase /// /// The collection of objects to search. is used to search the collection. /// - public IEnumerable Collection { get; set; } + public IList Collection { get; set; } /// /// Constructs a new CollectionNavigator. @@ -22,18 +23,19 @@ public CollectionNavigator () { } /// Constructs a new CollectionNavigator for the given collection. /// /// - public CollectionNavigator (IEnumerable collection) => Collection = collection; + public CollectionNavigator (IList collection) => Collection = collection; /// - protected override IEnumerable> GetCollection () + protected override object ElementAt (int idx) { - if (Collection == null) { - throw new InvalidOperationException ("Collection is null"); - } - - return Collection.Select ((item, idx) => KeyValuePair.Create(idx, item)); + return Collection[idx]; } + /// + protected override int GetCollectionLength () + { + return Collection.Count; + } } } diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index 5d15b4815b..660e3036ac 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -148,43 +148,60 @@ internal int GetNextMatchingItem (int currentIndex, string search, bool minimize return -1; } - // find indexes of items that start with the search text - int [] matchingIndexes = GetCollection() - .Where (k => k.Value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false) - .Select (k => k.Key) - .ToArray (); - - // if there are items beginning with search - if (matchingIndexes.Length > 0) { - // is one of them currently selected? - var currentlySelected = Array.IndexOf (matchingIndexes, currentIndex); - - if (currentlySelected == -1) { - // we are not currently selecting any item beginning with the search - // so jump to first item in list that begins with the letter - return matchingIndexes [0]; - } else { + var collectionLength = GetCollectionLength(); + + if(currentIndex != -1 && currentIndex < collectionLength && IsMatch(search, ElementAt(currentIndex))) + { + // we are already at a match + if (minimizeMovement) { + // if we would rather not jump around (e.g. user is typing lots of text to get this match) + return currentIndex; + } - // the current index is part of the matching collection - if (minimizeMovement) { - // if we would rather not jump around (e.g. user is typing lots of text to get this match) - return matchingIndexes [currentlySelected]; + for (int i = 1 ; i < collectionLength;i++) + { + //circular + var idxCandidate = (i + currentIndex) % collectionLength; + if(IsMatch(search, ElementAt(idxCandidate))) + { + return idxCandidate; } + } - // cycle to next (circular) - return matchingIndexes [(currentlySelected + 1) % matchingIndexes.Length]; + // nothing else starts with the search term + return currentIndex; + } + else + { + // search terms no longer match the current selection or there is none + for (int i = 0 ; i < collectionLength;i++) + { + if(IsMatch(search, ElementAt(i))) + { + return i; + } } + + // Nothing matches + return -1; } + } + + /// + /// Return the number of elements in the collection + /// + protected abstract int GetCollectionLength (); - // nothing starts with the search - return -1; + private bool IsMatch (string search, object value) + { + return value?.ToString ().StartsWith (search, StringComparison.InvariantCultureIgnoreCase) ?? false; } /// - /// Returns the index and value of each item that is to be considered for searching. + /// Returns the collection being navigated element at . /// /// - protected abstract IEnumerable> GetCollection (); + protected abstract object ElementAt (int idx); private void ClearSearchString () { diff --git a/Terminal.Gui/Views/ListView.cs b/Terminal.Gui/Views/ListView.cs index ac90638aec..725a504616 100644 --- a/Terminal.Gui/Views/ListView.cs +++ b/Terminal.Gui/Views/ListView.cs @@ -110,7 +110,7 @@ public IListDataSource Source { get => source; set { source = value; - KeystrokeNavigator.Collection = source?.ToList ()?.Cast (); + KeystrokeNavigator.Collection = source?.ToList (); top = 0; selected = -1; lastSelectedItem = -1; From f627a63b6378eb7fd1f1a3156a67b03bd73da6d7 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 29 Apr 2023 13:31:33 +0100 Subject: [PATCH 3/8] Add TableCollectionNavigator --- Terminal.Gui/Text/TableCollectionNavigator.cs | 47 +++++++++++++++++++ Terminal.Gui/Views/FileDialog.cs | 28 ++--------- 2 files changed, 50 insertions(+), 25 deletions(-) create mode 100644 Terminal.Gui/Text/TableCollectionNavigator.cs diff --git a/Terminal.Gui/Text/TableCollectionNavigator.cs b/Terminal.Gui/Text/TableCollectionNavigator.cs new file mode 100644 index 0000000000..a63f6c1e82 --- /dev/null +++ b/Terminal.Gui/Text/TableCollectionNavigator.cs @@ -0,0 +1,47 @@ + + +namespace Terminal.Gui { + + /// + /// Collection navigator for cycling selections in a . + /// + public class TableCollectionNavigator : CollectionNavigatorBase + { + readonly TableView tableView; + + /// + /// Gets or sets a value indicating whether the + /// should be respected (defaults to true). + /// + public bool RespectStyles {get;set;} = true; + + /// + /// Creates a new instance for navigating the data in the wrapped . + /// + public TableCollectionNavigator(TableView tableView) + { + this.tableView = tableView; + } + + /// + protected override object ElementAt (int idx) + { + var col = tableView.SelectedColumn; + var rawValue = tableView.Table[idx,col]; + + if(!RespectStyles) + { + return rawValue; + } + + var style = this.tableView.Style.GetColumnStyleIfAny(col); + return style.RepresentationGetter(rawValue); + } + + /// + protected override int GetCollectionLength () + { + return tableView.Table.Rows; + } + } +} diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 6d18bebadd..21bf91c7a6 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -100,7 +100,7 @@ public partial class FileDialog : Dialog { private Button btnUp; private string feedback; - private CollectionNavigator collectionNavigator = new CollectionNavigator (); + private TableCollectionNavigator collectionNavigator; private TextField tbFind; private SpinnerView spinnerView; @@ -306,6 +306,8 @@ public FileDialog (IFileSystem fileSystem) } }; + collectionNavigator = new TableCollectionNavigator (tableView); + this.tableView.Style.ShowHorizontalHeaderOverline = true; this.tableView.Style.ShowVerticalCellLines = true; this.tableView.Style.ShowVerticalHeaderLines = true; @@ -535,24 +537,6 @@ private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent) } } - private void UpdateCollectionNavigator () - { - tableView.EnsureValidSelection (); - var col = tableView.SelectedColumn; - var style = tableView.Style.GetColumnStyleIfAny (col); - - - var collection = dtFiles - .Rows - .Cast () - .Select ((o, idx) => col == 0 ? - RowToStats(idx).FileSystemInfo.Name : - style.GetRepresentation (o [0])?.TrimStart('.')) - .ToArray (); - - collectionNavigator = new CollectionNavigator (collection); - } - /// /// Gets or Sets which type can be selected. /// Defaults to (i.e. or @@ -928,10 +912,6 @@ private void TableView_SelectedCellChanged (object sender, SelectedCellChangedEv this.pushingState = false; } - - if (obj.NewCol != obj.OldCol) { - UpdateCollectionNavigator (); - } } private bool TableView_KeyUp (KeyEvent keyEvent) @@ -1234,7 +1214,6 @@ private void WriteStateToTableView () this.sorter.ApplySort (); this.tableView.Update (); - UpdateCollectionNavigator (); } private void BuildRow (int idx) @@ -1456,7 +1435,6 @@ internal void ApplySort () } this.tableView.Update (); - dlg.UpdateCollectionNavigator (); } private static string StripArrows (string columnName) From 789c8719e5a62e08b750ed8fe7603cc8d25928e3 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 29 Apr 2023 13:51:35 +0100 Subject: [PATCH 4/8] Make TableCollectionNavigator a core part of TableView --- Terminal.Gui/Text/TableCollectionNavigator.cs | 2 +- Terminal.Gui/Views/FileDialog.cs | 36 ----------------- Terminal.Gui/Views/TableView/TableView.cs | 40 +++++++++++++++++++ 3 files changed, 41 insertions(+), 37 deletions(-) diff --git a/Terminal.Gui/Text/TableCollectionNavigator.cs b/Terminal.Gui/Text/TableCollectionNavigator.cs index a63f6c1e82..e271a27280 100644 --- a/Terminal.Gui/Text/TableCollectionNavigator.cs +++ b/Terminal.Gui/Text/TableCollectionNavigator.cs @@ -35,7 +35,7 @@ protected override object ElementAt (int idx) } var style = this.tableView.Style.GetColumnStyleIfAny(col); - return style.RepresentationGetter(rawValue); + return style?.RepresentationGetter?.Invoke(rawValue) ?? rawValue; } /// diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 21bf91c7a6..3aa2deb6ef 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -99,9 +99,6 @@ public partial class FileDialog : Dialog { private Button btnBack; private Button btnUp; private string feedback; - - private TableCollectionNavigator collectionNavigator; - private TextField tbFind; private SpinnerView spinnerView; private MenuBar allowedTypeMenuBar; @@ -237,14 +234,6 @@ public FileDialog (IFileSystem fileSystem) if (k.Handled) { return; } - - if (this.tableView.HasFocus && - !k.KeyEvent.Key.HasFlag (Key.CtrlMask) && - !k.KeyEvent.Key.HasFlag (Key.AltMask) && - char.IsLetterOrDigit ((char)k.KeyEvent.KeyValue)) { - CycleToNextTableEntryBeginningWith (k); - } - }; this.treeView = new TreeView () { @@ -306,8 +295,6 @@ public FileDialog (IFileSystem fileSystem) } }; - collectionNavigator = new TableCollectionNavigator (tableView); - this.tableView.Style.ShowHorizontalHeaderOverline = true; this.tableView.Style.ShowVerticalCellLines = true; this.tableView.Style.ShowVerticalHeaderLines = true; @@ -514,29 +501,6 @@ private void ClearFeedback () feedback = null; } - private void CycleToNextTableEntryBeginningWith (KeyEventEventArgs keyEvent) - { - if (tableView.Table.Rows == 0) { - return; - } - - var row = tableView.SelectedRow; - - // There is a multi select going on and not just for the current row - if (tableView.GetAllSelectedCells ().Any (c => c.Y != row)) { - return; - } - - int match = collectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyEvent.KeyValue); - - if (match != -1) { - tableView.SelectedRow = match; - tableView.EnsureValidSelection (); - tableView.EnsureSelectedCellIsVisible (); - keyEvent.Handled = true; - } - } - /// /// Gets or Sets which type can be selected. /// Defaults to (i.e. or diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 30ad804d19..1e4e8a5421 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -156,6 +156,12 @@ public Key CellActivationKey { } } + /// + /// Navigator for cycling the selected item in the table by typing. + /// Set to null to disable this feature. + /// + public TableCollectionNavigator CollectionNavigator { get; set;} + /// /// Initialzies a class using layout. /// @@ -171,6 +177,8 @@ public TableView (ITableSource table) : this () public TableView () : base () { CanFocus = true; + + this.CollectionNavigator = new TableCollectionNavigator(this); // Things this view knows how to do AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; }); @@ -737,6 +745,38 @@ public override bool ProcessKey (KeyEvent keyEvent) return true; } + if (CollectionNavigator != null && + this.HasFocus && + Table.Rows != 0 && + Terminal.Gui.CollectionNavigator.IsCompatibleKey (keyEvent) && + !keyEvent.Key.HasFlag (Key.CtrlMask) && + !keyEvent.Key.HasFlag (Key.AltMask) && + char.IsLetterOrDigit ((char)keyEvent.KeyValue)) + { + return CycleToNextTableEntryBeginningWith (keyEvent); + } + + return false; + } + + private bool CycleToNextTableEntryBeginningWith (KeyEvent keyEvent) + { + var row = SelectedRow; + + // There is a multi select going on and not just for the current row + if (GetAllSelectedCells ().Any (c => c.Y != row)) { + return false; + } + + int match = CollectionNavigator.GetNextMatchingItem (row, (char)keyEvent.KeyValue); + + if (match != -1) { + SelectedRow = match; + EnsureValidSelection (); + EnsureSelectedCellIsVisible (); + return true; + } + return false; } From ac683c9858f55dba00d69d8a90042d29701c2ff4 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 29 Apr 2023 14:00:31 +0100 Subject: [PATCH 5/8] Fix bad merge --- Terminal.Gui/Views/FileDialog.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 241039789a..6c51c10eb2 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -1269,7 +1269,6 @@ internal void ApplySort () State.Children = ordered.ToArray(); this.tableView.Update (); - UpdateCollectionNavigator (); } private void SortColumn (int clickedCol) From 1b2f75c9b2d1fb279b6516f4f29476296413234c Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 29 Apr 2023 15:54:44 +0100 Subject: [PATCH 6/8] Added tests for tableview collection navigator --- UnitTests/Views/TableViewTests.cs | 87 +++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index acd0b7fa84..349d89fb47 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -2517,6 +2517,93 @@ public void TestEnumerableDataSource_BasicTypes() │Single│System │System.ValueType │"; TestHelpers.AssertDriverContentsAre (expected, output); + } + [Fact, AutoInitShutdown] + public void Test_CollectionNavigator () + { + var tv = new TableView (); + tv.ColorScheme = Colors.TopLevel; + tv.Bounds = new Rect (0, 0, 50, 7); + + tv.Table = new EnumerableTableSource ( + new string [] { "fish", "troll", "trap","zoo" }, + new () { + { "Name", (t)=>t}, + { "EndsWith", (t)=>t.Last()} + }); + + tv.LayoutSubviews (); + + tv.Redraw (tv.Bounds); + + string expected = + @" +┌─────┬──────────────────────────────────────────┐ +│Name │EndsWith │ +├─────┼──────────────────────────────────────────┤ +│fish │h │ +│troll│l │ +│trap │p │ +│zoo │o │"; + + TestHelpers.AssertDriverContentsAre (expected, output); + + Assert.Equal (0, tv.SelectedRow); + + // this test assumes no focus + Assert.False (tv.HasFocus); + + // already on fish + tv.ProcessKey (new KeyEvent {Key = Key.f}); + Assert.Equal (0, tv.SelectedRow); + + // not focused + tv.ProcessKey (new KeyEvent { Key = Key.z }); + Assert.Equal (0, tv.SelectedRow); + + // ensure that TableView has the input focus + Application.Top.Add (tv); + Application.Begin (Application.Top); + + Application.Top.FocusFirst (); + Assert.True (tv.HasFocus); + + // already on fish + tv.ProcessKey (new KeyEvent { Key = Key.f }); + Assert.Equal (0, tv.SelectedRow); + + // move to zoo + tv.ProcessKey (new KeyEvent { Key = Key.z }); + Assert.Equal (3, tv.SelectedRow); + + // move to troll + tv.ProcessKey (new KeyEvent { Key = Key.t }); + Assert.Equal (1, tv.SelectedRow); + + // move to trap + tv.ProcessKey (new KeyEvent { Key = Key.t }); + Assert.Equal (2, tv.SelectedRow); + + // change columns to navigate by column 2 + Assert.Equal (0, tv.SelectedColumn); + Assert.Equal (2, tv.SelectedRow); + tv.ProcessKey (new KeyEvent { Key = Key.CursorRight }); + Assert.Equal (1, tv.SelectedColumn); + Assert.Equal (2, tv.SelectedRow); + + // nothing ends with t so stay where you are + tv.ProcessKey (new KeyEvent { Key = Key.t }); + Assert.Equal (2, tv.SelectedRow); + + //jump to fish which ends in h + tv.ProcessKey (new KeyEvent { Key = Key.h }); + Assert.Equal (0, tv.SelectedRow); + + // jump to zoo which ends in o + tv.ProcessKey (new KeyEvent { Key = Key.o }); + Assert.Equal (3, tv.SelectedRow); + + } private TableView GetTwoRowSixColumnTable () { From a5652aae0cba62cee015544c04bf06811719ccd5 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 29 Apr 2023 16:26:36 +0100 Subject: [PATCH 7/8] Add FileDialogCollectionNavigator which ignores . and directory separator prefixes on file names --- Terminal.Gui/Text/TableCollectionNavigator.cs | 42 +++++++------------ Terminal.Gui/Views/FileDialog.cs | 27 +++++++++++- Terminal.Gui/Views/TableView/TableView.cs | 2 +- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/Terminal.Gui/Text/TableCollectionNavigator.cs b/Terminal.Gui/Text/TableCollectionNavigator.cs index e271a27280..29cddbe6d5 100644 --- a/Terminal.Gui/Text/TableCollectionNavigator.cs +++ b/Terminal.Gui/Text/TableCollectionNavigator.cs @@ -2,43 +2,31 @@ namespace Terminal.Gui { - /// - /// Collection navigator for cycling selections in a . - /// - public class TableCollectionNavigator : CollectionNavigatorBase - { + /// + /// Collection navigator for cycling selections in a . + /// + public class TableCollectionNavigator : CollectionNavigatorBase { readonly TableView tableView; - /// - /// Gets or sets a value indicating whether the - /// should be respected (defaults to true). - /// - public bool RespectStyles {get;set;} = true; - - /// - /// Creates a new instance for navigating the data in the wrapped . - /// - public TableCollectionNavigator(TableView tableView) - { + /// + /// Creates a new instance for navigating the data in the wrapped . + /// + public TableCollectionNavigator (TableView tableView) + { this.tableView = tableView; } - /// + /// protected override object ElementAt (int idx) { - var col = tableView.SelectedColumn; - var rawValue = tableView.Table[idx,col]; - - if(!RespectStyles) - { - return rawValue; - } + var col = tableView.SelectedColumn; + var rawValue = tableView.Table [idx, col]; - var style = this.tableView.Style.GetColumnStyleIfAny(col); - return style?.RepresentationGetter?.Invoke(rawValue) ?? rawValue; + var style = this.tableView.Style.GetColumnStyleIfAny (col); + return style?.RepresentationGetter?.Invoke (rawValue) ?? rawValue; } - /// + /// protected override int GetCollectionLength () { return tableView.Table.Rows; diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 6c51c10eb2..7294787b54 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -211,12 +211,12 @@ public FileDialog (IFileSystem fileSystem) // this.splitContainer.Border.BorderStyle = BorderStyle.None; this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false; - this.tableView = new TableView () { + this.tableView = new TableView { Width = Dim.Fill (), Height = Dim.Fill (), FullRowSelect = true, + CollectionNavigator = new FileDialogCollectionNavigator (this) }; - this.tableView.AddKeyBinding (Key.Space, Command.ToggleChecked); this.tableView.MouseClick += OnTableViewMouseClick; Style.TableStyle = tableView.Style; @@ -1478,5 +1478,28 @@ internal bool Cancel () return !alreadyCancelled; } } + internal class FileDialogCollectionNavigator : CollectionNavigatorBase { + private FileDialog fileDialog; + + public FileDialogCollectionNavigator (FileDialog fileDialog) + { + this.fileDialog = fileDialog; + } + + protected override object ElementAt (int idx) + { + var val = FileDialogTableSource.GetRawColumnValue (fileDialog.tableView.SelectedColumn, fileDialog.State?.Children [idx]); + if(val == null) { + return string.Empty; + } + + return val.ToString().Trim('.'); + } + + protected override int GetCollectionLength () + { + return fileDialog.State?.Children.Length ?? 0; + } + } } } \ No newline at end of file diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 1e4e8a5421..5934cb6b8f 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -160,7 +160,7 @@ public Key CellActivationKey { /// Navigator for cycling the selected item in the table by typing. /// Set to null to disable this feature. /// - public TableCollectionNavigator CollectionNavigator { get; set;} + public CollectionNavigatorBase CollectionNavigator { get; set;} /// /// Initialzies a class using layout. From 51ed711e7b881451f3c239dd4194909b235bb671 Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 29 Apr 2023 16:28:12 +0100 Subject: [PATCH 8/8] whitespace fixes --- Terminal.Gui/Text/CollectionNavigator.cs | 5 +- Terminal.Gui/Text/CollectionNavigatorBase.cs | 27 ++--- Terminal.Gui/Views/FileDialog.cs | 88 +++++++--------- Terminal.Gui/Views/TableView/TableView.cs | 103 +++++++++---------- UnitTests/Views/TableViewTests.cs | 46 ++++----- 5 files changed, 121 insertions(+), 148 deletions(-) diff --git a/Terminal.Gui/Text/CollectionNavigator.cs b/Terminal.Gui/Text/CollectionNavigator.cs index 7a23934f4e..276dd7fb7b 100644 --- a/Terminal.Gui/Text/CollectionNavigator.cs +++ b/Terminal.Gui/Text/CollectionNavigator.cs @@ -7,8 +7,7 @@ namespace Terminal.Gui { /// /// This implementation is based on a static of objects. - public class CollectionNavigator : CollectionNavigatorBase - { + public class CollectionNavigator : CollectionNavigatorBase { /// /// The collection of objects to search. is used to search the collection. /// @@ -28,7 +27,7 @@ public CollectionNavigator () { } /// protected override object ElementAt (int idx) { - return Collection[idx]; + return Collection [idx]; } /// diff --git a/Terminal.Gui/Text/CollectionNavigatorBase.cs b/Terminal.Gui/Text/CollectionNavigatorBase.cs index 660e3036ac..1d65116dc6 100644 --- a/Terminal.Gui/Text/CollectionNavigatorBase.cs +++ b/Terminal.Gui/Text/CollectionNavigatorBase.cs @@ -112,7 +112,7 @@ public int GetNextMatchingItem (int currentIndex, char keyStruck) if (idxCandidate == currentIndex || idxCandidate == -1) { // clear history and treat as a fresh letter ClearSearchString (); - + // match on the fresh letter alone SearchString = new string (keyStruck, 1); idxCandidate = GetNextMatchingItem (currentIndex, SearchString); @@ -148,40 +148,33 @@ internal int GetNextMatchingItem (int currentIndex, string search, bool minimize return -1; } - var collectionLength = GetCollectionLength(); + var collectionLength = GetCollectionLength (); - if(currentIndex != -1 && currentIndex < collectionLength && IsMatch(search, ElementAt(currentIndex))) - { + if (currentIndex != -1 && currentIndex < collectionLength && IsMatch (search, ElementAt (currentIndex))) { // we are already at a match if (minimizeMovement) { // if we would rather not jump around (e.g. user is typing lots of text to get this match) return currentIndex; } - for (int i = 1 ; i < collectionLength;i++) - { + for (int i = 1; i < collectionLength; i++) { //circular var idxCandidate = (i + currentIndex) % collectionLength; - if(IsMatch(search, ElementAt(idxCandidate))) - { + if (IsMatch (search, ElementAt (idxCandidate))) { return idxCandidate; } } // nothing else starts with the search term return currentIndex; - } - else - { + } else { // search terms no longer match the current selection or there is none - for (int i = 0 ; i < collectionLength;i++) - { - if(IsMatch(search, ElementAt(i))) - { + for (int i = 0; i < collectionLength; i++) { + if (IsMatch (search, ElementAt (i))) { return i; } } - + // Nothing matches return -1; } @@ -190,7 +183,7 @@ internal int GetNextMatchingItem (int currentIndex, string search, bool minimize /// /// Return the number of elements in the collection /// - protected abstract int GetCollectionLength (); + protected abstract int GetCollectionLength (); private bool IsMatch (string search, object value) { diff --git a/Terminal.Gui/Views/FileDialog.cs b/Terminal.Gui/Views/FileDialog.cs index 7294787b54..7457fdfbeb 100644 --- a/Terminal.Gui/Views/FileDialog.cs +++ b/Terminal.Gui/Views/FileDialog.cs @@ -104,7 +104,7 @@ public partial class FileDialog : Dialog { private MenuItem [] allowedTypeMenuItems; private int currentSortColumn; - + private bool currentSortIsAsc = true; /// @@ -125,7 +125,7 @@ public partial class FileDialog : Dialog { /// /// Initializes a new instance of the class. /// - public FileDialog () : this(new FileSystem()) + public FileDialog () : this (new FileSystem ()) { } @@ -181,7 +181,7 @@ public FileDialog (IFileSystem fileSystem) this.btnBack.Clicked += (s, e) => this.history.Back (); this.btnForward = new Button () { X = Pos.Right (btnBack) + 1, Y = 1, NoPadding = true }; - btnForward.Text = GetForwardButtonText(); + btnForward.Text = GetForwardButtonText (); this.btnForward.Clicked += (s, e) => this.history.Forward (); this.tbPath = new TextField { @@ -208,7 +208,7 @@ public FileDialog (IFileSystem fileSystem) Height = Dim.Fill (1), }; this.splitContainer.SetSplitterPos (0, 30); -// this.splitContainer.Border.BorderStyle = BorderStyle.None; + // this.splitContainer.Border.BorderStyle = BorderStyle.None; this.splitContainer.Tiles.ElementAt (0).ContentView.Visible = false; this.tableView = new TableView { @@ -277,7 +277,7 @@ public FileDialog (IFileSystem fileSystem) var newState = !tile.ContentView.Visible; tile.ContentView.Visible = newState; this.btnToggleSplitterCollapse.Text = GetToggleSplitterText (newState); - this.LayoutSubviews(); + this.LayoutSubviews (); }; tbFind = new TextField { @@ -300,12 +300,12 @@ public FileDialog (IFileSystem fileSystem) o.Handled = true; } - if(o.KeyEvent.Key == Key.Esc) { - if(CancelSearch()) { + if (o.KeyEvent.Key == Key.Esc) { + if (CancelSearch ()) { o.Handled = true; } } - if(tbFind.CursorIsAtEnd()) { + if (tbFind.CursorIsAtEnd ()) { NavigateIf (o, Key.CursorRight, btnCancel); } if (tbFind.CursorIsAtStart ()) { @@ -410,7 +410,7 @@ private string GetUpButtonText () private string GetToggleSplitterText (bool isExpanded) { - return isExpanded ? + return isExpanded ? new string ((char)Driver.LeftArrow, 2) : new string ((char)Driver.RightArrow, 2); } @@ -533,7 +533,7 @@ private void ClearFeedback () { feedback = null; } - + /// /// Gets or Sets which type can be selected. /// Defaults to (i.e. or @@ -628,11 +628,11 @@ public override void OnLoaded () // May have been updated after instance was constructed this.btnOk.Text = Style.OkButtonText; - this.btnUp.Text = this.GetUpButtonText(); - this.btnBack.Text = this.GetBackButtonText(); - this.btnForward.Text = this.GetForwardButtonText(); - this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText(false); - + this.btnUp.Text = this.GetUpButtonText (); + this.btnBack.Text = this.GetBackButtonText (); + this.btnForward.Text = this.GetForwardButtonText (); + this.btnToggleSplitterCollapse.Text = this.GetToggleSplitterText (false); + tbPath.Autocomplete.ColorScheme.Normal = Attribute.Make (Color.Black, tbPath.ColorScheme.Normal.Background); treeView.AddObjects (Style.TreeRootGetter ()); @@ -670,7 +670,7 @@ public override void OnLoaded () }; allowedTypeMenuBar.DrawContentComplete += (s, e) => { - + allowedTypeMenuBar.Move (e.Rect.Width - 1, 0); Driver.AddRune (Driver.DownArrow); @@ -768,7 +768,7 @@ private void Accept (IEnumerable toMultiAccept) // Don't include ".." (IsParent) in multiselections this.MultiSelected = toMultiAccept - .Where(s=>!s.IsParent) + .Where (s => !s.IsParent) .Select (s => s.FileSystemInfo.FullName) .ToList ().AsReadOnly (); @@ -796,8 +796,7 @@ private void Accept (IFileInfo f) private void Accept (bool allowMulti) { - if(allowMulti && TryAcceptMulti()) - { + if (allowMulti && TryAcceptMulti ()) { return; } @@ -942,8 +941,7 @@ private bool TableView_KeyUp (KeyEvent keyEvent) private void CellActivate (object sender, CellActivatedEventArgs obj) { - if(TryAcceptMulti()) - { + if (TryAcceptMulti ()) { return; } @@ -963,20 +961,16 @@ private bool TryAcceptMulti () { var multi = this.MultiRowToStats (); string reason = null; - - if (!multi.Any ()) - { + + if (!multi.Any ()) { return false; } - + if (multi.All (m => this.IsCompatibleWithOpenMode ( - m.FileSystemInfo.FullName, out reason))) - { + m.FileSystemInfo.FullName, out reason))) { this.Accept (multi); return true; - } - else - { + } else { if (reason != null) { feedback = reason; SetNeedsDisplay (); @@ -1108,12 +1102,10 @@ private void PushState (FileDialogState newState, bool addCurrentStateToHistory, this.tbPath.Autocomplete.ClearSuggestions (); - if(pathText != null) - { + if (pathText != null) { this.tbPath.Text = pathText; this.tbPath.MoveEnd (); - } - else + } else if (setPathText) { this.tbPath.Text = newState.Directory.FullName; this.tbPath.MoveEnd (); @@ -1203,7 +1195,7 @@ private FileSystemInfoStats RowToStats (int rowIndex) { return this.State?.Children [rowIndex]; } - + private void PathChanged () { // avoid re-entry @@ -1235,10 +1227,10 @@ private IDirectoryInfo StringToDirectoryInfo (string path) // where the FullName is in fact the current working directory. // really not what most users would expect if (Regex.IsMatch (path, "^\\w:$")) { - return fileSystem.DirectoryInfo.New(path + System.IO.Path.DirectorySeparatorChar); + return fileSystem.DirectoryInfo.New (path + System.IO.Path.DirectorySeparatorChar); } - return fileSystem.DirectoryInfo.New(path); + return fileSystem.DirectoryInfo.New (path); } /// @@ -1247,7 +1239,7 @@ private IDirectoryInfo StringToDirectoryInfo (string path) /// internal void RestoreSelection (IFileSystemInfo toRestore) { - tableView.SelectedRow = State.Children.IndexOf (r=>r.FileSystemInfo == toRestore); + tableView.SelectedRow = State.Children.IndexOf (r => r.FileSystemInfo == toRestore); tableView.EnsureSelectedCellIsVisible (); } @@ -1258,15 +1250,15 @@ internal void ApplySort () // This portion is never reordered (aways .. at top then folders) var forcedOrder = stats .OrderByDescending (f => f.IsParent) - .ThenBy (f => f.IsDir() ? -1:100); + .ThenBy (f => f.IsDir () ? -1 : 100); // This portion is flexible based on the column clicked (e.g. alphabetical) - var ordered = + var ordered = this.currentSortIsAsc ? - forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue(currentSortColumn,f)): + forcedOrder.ThenBy (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f)) : forcedOrder.ThenByDescending (f => FileDialogTableSource.GetRawColumnValue (currentSortColumn, f)); - State.Children = ordered.ToArray(); + State.Children = ordered.ToArray (); this.tableView.Update (); } @@ -1275,7 +1267,7 @@ private void SortColumn (int clickedCol) { this.GetProposedNewSortOrder (clickedCol, out var isAsc); this.SortColumn (clickedCol, isAsc); - this.tableView.Table = new FileDialogTableSource(State,Style,currentSortColumn,currentSortIsAsc); + this.tableView.Table = new FileDialogTableSource (State, Style, currentSortColumn, currentSortIsAsc); } internal void SortColumn (int col, bool isAsc) @@ -1292,7 +1284,7 @@ private string GetProposedNewSortOrder (int clickedCol, out bool isAsc) // work out new sort order if (this.currentSortColumn == clickedCol && this.currentSortIsAsc) { isAsc = false; - return $"{tableView.Table.ColumnNames[clickedCol]} DESC"; + return $"{tableView.Table.ColumnNames [clickedCol]} DESC"; } else { isAsc = true; return $"{tableView.Table.ColumnNames [clickedCol]} ASC"; @@ -1349,7 +1341,7 @@ private void HideColumn (int clickedCol) style.Visible = false; this.tableView.Update (); } - + /// /// State representing a recursive search from /// downwards. @@ -1470,7 +1462,7 @@ internal override void RefreshChildren () /// internal bool Cancel () { - var alreadyCancelled = token.IsCancellationRequested || cancel; + var alreadyCancelled = token.IsCancellationRequested || cancel; cancel = true; token.Cancel (); @@ -1489,11 +1481,11 @@ public FileDialogCollectionNavigator (FileDialog fileDialog) protected override object ElementAt (int idx) { var val = FileDialogTableSource.GetRawColumnValue (fileDialog.tableView.SelectedColumn, fileDialog.State?.Children [idx]); - if(val == null) { + if (val == null) { return string.Empty; } - return val.ToString().Trim('.'); + return val.ToString ().Trim ('.'); } protected override int GetCollectionLength () diff --git a/Terminal.Gui/Views/TableView/TableView.cs b/Terminal.Gui/Views/TableView/TableView.cs index 5934cb6b8f..7ac9d422b7 100644 --- a/Terminal.Gui/Views/TableView/TableView.cs +++ b/Terminal.Gui/Views/TableView/TableView.cs @@ -160,7 +160,7 @@ public Key CellActivationKey { /// Navigator for cycling the selected item in the table by typing. /// Set to null to disable this feature. /// - public CollectionNavigatorBase CollectionNavigator { get; set;} + public CollectionNavigatorBase CollectionNavigator { get; set; } /// /// Initialzies a class using layout. @@ -177,8 +177,8 @@ public TableView (ITableSource table) : this () public TableView () : base () { CanFocus = true; - - this.CollectionNavigator = new TableCollectionNavigator(this); + + this.CollectionNavigator = new TableCollectionNavigator (this); // Things this view knows how to do AddCommand (Command.Right, () => { ChangeSelectionByOffset (1, 0, false); return true; }); @@ -293,9 +293,9 @@ public override void Redraw (Rect bounds) continue; // No more data - if(rowToRender >= Table.Rows) { + if (rowToRender >= Table.Rows) { - if(rowToRender == Table.Rows && Style.ShowHorizontalBottomline) { + if (rowToRender == Table.Rows && Style.ShowHorizontalBottomline) { RenderBottomLine (line, bounds.Width, columnsToRender); } @@ -391,7 +391,7 @@ private void RenderHeaderMidline (int row, ColumnToRender [] columnsToRender) var current = columnsToRender [i]; var colStyle = Style.GetColumnStyleIfAny (current.Column); - var colName = table.ColumnNames[current.Column]; + var colName = table.ColumnNames [current.Column]; RenderSeparator (current.X - 1, row, true); @@ -515,7 +515,7 @@ private void RenderBottomLine (int row, int availableWidth, ColumnToRender [] co } // if the next column is the start of a header else if (columnsToRender.Any (r => r.X == c + 1)) { - rune = Driver.BottomTee; + rune = Driver.BottomTee; } else if (c == availableWidth - 1) { // for the last character in the table @@ -618,7 +618,7 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen if (!FullRowSelect) Driver.SetAttribute (Enabled ? rowScheme.Normal : rowScheme.Disabled); - if(style.AlwaysUseNormalColorForVerticalCellLines && style.ShowVerticalCellLines) { + if (style.AlwaysUseNormalColorForVerticalCellLines && style.ShowVerticalCellLines) { Driver.SetAttribute (rowScheme.Normal); } @@ -638,7 +638,7 @@ private void RenderRow (int row, int rowToRender, ColumnToRender [] columnsToRen AddRune (0, row, Driver.VLine); AddRune (Bounds.Width - 1, row, Driver.VLine); } - + } /// @@ -702,7 +702,7 @@ void AddRuneAt (ConsoleDriver d, int col, int row, Rune ch) private string TruncateOrPad (object originalCellValue, string representation, int availableHorizontalSpace, ColumnStyle colStyle) { if (string.IsNullOrEmpty (representation)) - return new string(' ',availableHorizontalSpace); + return new string (' ', availableHorizontalSpace); // if value is not wide enough if (representation.Sum (c => Rune.ColumnWidth (c)) < availableHorizontalSpace) { @@ -745,14 +745,13 @@ public override bool ProcessKey (KeyEvent keyEvent) return true; } - if (CollectionNavigator != null && - this.HasFocus && + if (CollectionNavigator != null && + this.HasFocus && Table.Rows != 0 && Terminal.Gui.CollectionNavigator.IsCompatibleKey (keyEvent) && !keyEvent.Key.HasFlag (Key.CtrlMask) && !keyEvent.Key.HasFlag (Key.AltMask) && - char.IsLetterOrDigit ((char)keyEvent.KeyValue)) - { + char.IsLetterOrDigit ((char)keyEvent.KeyValue)) { return CycleToNextTableEntryBeginningWith (keyEvent); } @@ -801,7 +800,7 @@ public void SetSelection (int col, int row, bool extendExistingSelection) if (extendExistingSelection) { // If we are extending current selection but there isn't one - if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All(m=>m.IsToggled)) { + if (MultiSelectedRegions.Count == 0 || MultiSelectedRegions.All (m => m.IsToggled)) { // Create a new region between the old active cell and the new cell var rect = CreateTableSelection (SelectedColumn, SelectedRow, col, row); MultiSelectedRegions.Push (rect); @@ -968,14 +967,13 @@ public void SelectAll () /// public IEnumerable GetAllSelectedCells () { - if (TableIsNullOrInvisible () || Table.Rows == 0) - { - return Enumerable.Empty(); + if (TableIsNullOrInvisible () || Table.Rows == 0) { + return Enumerable.Empty (); } EnsureValidSelection (); - var toReturn = new HashSet(); + var toReturn = new HashSet (); // If there are one or more rectangular selections if (MultiSelect && MultiSelectedRegions.Any ()) { @@ -990,11 +988,11 @@ public IEnumerable GetAllSelectedCells () for (int y = yMin; y < yMax; y++) { for (int x = xMin; x < xMax; x++) { if (IsSelected (x, y)) { - toReturn.Add(new Point (x, y)); + toReturn.Add (new Point (x, y)); } } } - } + } // if there are no region selections then it is just the active cell @@ -1002,14 +1000,14 @@ public IEnumerable GetAllSelectedCells () if (FullRowSelect) { // all cells in active row are selected for (int x = 0; x < Table.Columns; x++) { - toReturn.Add(new Point (x, SelectedRow)); + toReturn.Add (new Point (x, SelectedRow)); } } else { // Not full row select and no multi selections - toReturn.Add(new Point (SelectedColumn, SelectedRow)); + toReturn.Add (new Point (SelectedColumn, SelectedRow)); } - return toReturn; + return toReturn; } /// @@ -1041,8 +1039,8 @@ private void ToggleCurrentCellSelection () return; } - var regions = GetMultiSelectedRegionsContaining(selectedColumn, selectedRow).ToArray(); - var toggles = regions.Where(s=>s.IsToggled).ToArray (); + var regions = GetMultiSelectedRegionsContaining (selectedColumn, selectedRow).ToArray (); + var toggles = regions.Where (s => s.IsToggled).ToArray (); // Toggle it off if (toggles.Any ()) { @@ -1055,17 +1053,14 @@ private void ToggleCurrentCellSelection () MultiSelectedRegions.Push (region); } } else { - + // user is toggling selection within a rectangular // select. So toggle the full region - if(regions.Any()) - { - foreach(var r in regions) - { + if (regions.Any ()) { + foreach (var r in regions) { r.IsToggled = true; } - } - else{ + } else { // Toggle on a single cell selection MultiSelectedRegions.Push ( CreateTableSelection (selectedColumn, SelectedRow, selectedColumn, selectedRow, true) @@ -1100,8 +1095,7 @@ public bool IsSelected (int col, int row) return false; } - if(GetMultiSelectedRegionsContaining(col,row).Any()) - { + if (GetMultiSelectedRegionsContaining (col, row).Any ()) { return true; } @@ -1109,19 +1103,15 @@ public bool IsSelected (int col, int row) (col == SelectedColumn || FullRowSelect); } - private IEnumerable GetMultiSelectedRegionsContaining(int col, int row) + private IEnumerable GetMultiSelectedRegionsContaining (int col, int row) { - if(!MultiSelect) - { - return Enumerable.Empty(); + if (!MultiSelect) { + return Enumerable.Empty (); } - - if(FullRowSelect) - { + + if (FullRowSelect) { return MultiSelectedRegions.Where (r => r.Rect.Bottom > row && r.Rect.Top <= row); - } - else - { + } else { return MultiSelectedRegions.Where (r => r.Rect.Contains (col, row)); } } @@ -1428,7 +1418,7 @@ private bool TableIsNullOrInvisible () { return Table == null || Table.Columns <= 0 || - Enumerable.Range(0,Table.Columns).All ( + Enumerable.Range (0, Table.Columns).All ( c => (Style.GetColumnStyleIfAny (c)?.Visible ?? true) == false); } @@ -1464,8 +1454,8 @@ private bool TryGetNearestVisibleColumn (int columnIndex, bool lookRight, bool a } // get the column visibility by index (if no style visible is true) - bool [] columnVisibility = - Enumerable.Range(0,Table.Columns) + bool [] columnVisibility = + Enumerable.Range (0, Table.Columns) .Select (c => this.Style.GetColumnStyleIfAny (c)?.Visible ?? true) .ToArray (); @@ -1566,7 +1556,7 @@ public void EnsureSelectedCellIsVisible () /// protected virtual void OnSelectedCellChanged (SelectedCellChangedEventArgs args) { - SelectedCellChanged?.Invoke (this,args); + SelectedCellChanged?.Invoke (this, args); } /// @@ -1588,7 +1578,7 @@ private IEnumerable CalculateViewport (Rect bounds, int padding { if (TableIsNullOrInvisible ()) { return Enumerable.Empty (); - } + } var toReturn = new List (); int usedSpace = 0; @@ -1608,7 +1598,7 @@ private IEnumerable CalculateViewport (Rect bounds, int padding var lastColumn = Table.Columns - 1; // TODO : Maybe just a for loop? - foreach (var col in Enumerable.Range(0,Table.Columns).Skip (ColumnOffset)) { + foreach (var col in Enumerable.Range (0, Table.Columns).Skip (ColumnOffset)) { int startingIdxForCurrentHeader = usedSpace; var colStyle = Style.GetColumnStyleIfAny (col); @@ -1658,12 +1648,11 @@ private IEnumerable CalculateViewport (Rect bounds, int padding var isVeryLast = lastColumn == col; // there is space - toReturn.Add(new ColumnToRender (col, startingIdxForCurrentHeader, colWidth, isVeryLast)); + toReturn.Add (new ColumnToRender (col, startingIdxForCurrentHeader, colWidth, isVeryLast)); first = false; } - if(Style.ExpandLastColumn) - { + if (Style.ExpandLastColumn) { var last = toReturn.Last (); last.Width = Math.Max (last.Width, availableHorizontalSpace - last.X); } @@ -1688,7 +1677,7 @@ private bool ShouldRenderHeaders () /// private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colStyle) { - int spaceRequired = table.ColumnNames[col].Sum (c => Rune.ColumnWidth (c)); + int spaceRequired = table.ColumnNames [col].Sum (c => Rune.ColumnWidth (c)); // if table has no rows if (RowOffset < 0) @@ -1700,7 +1689,7 @@ private int CalculateMaxCellWidth (int col, int rowsToRender, ColumnStyle colSty //expand required space if cell is bigger than the last biggest cell or header spaceRequired = Math.Max ( spaceRequired, - GetRepresentation (Table [i,col], colStyle).Sum (c => Rune.ColumnWidth (c))); + GetRepresentation (Table [i, col], colStyle).Sum (c => Rune.ColumnWidth (c))); } // Don't require more space than the style allows @@ -1901,7 +1890,7 @@ public class TableStyle { /// public bool ShowHorizontalScrollIndicators { get; set; } = true; - + /// /// Gets or sets a flag indicating whether there should be a horizontal line after all the data /// in the table. Defaults to . diff --git a/UnitTests/Views/TableViewTests.cs b/UnitTests/Views/TableViewTests.cs index 349d89fb47..39786cf7fd 100644 --- a/UnitTests/Views/TableViewTests.cs +++ b/UnitTests/Views/TableViewTests.cs @@ -28,7 +28,7 @@ public void EnsureValidScrollOffsets_WithNoCells () Assert.Equal (0, tableView.ColumnOffset); // Set empty table - tableView.Table = new DataTableSource(new DataTable ()); + tableView.Table = new DataTableSource (new DataTable ()); // Since table has no rows or columns scroll offset should default to 0 tableView.EnsureValidScrollOffsets (); @@ -598,7 +598,7 @@ public void TableView_Activate () { string activatedValue = null; var tv = new TableView (BuildTable (1, 1)); - tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row,c.Col].ToString(); + tv.CellActivated += (s, c) => activatedValue = c.Table [c.Row, c.Col].ToString (); Application.Top.Add (tv); Application.Begin (Application.Top); @@ -907,7 +907,7 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) }; // when B is 2 use the custom highlight colour for the row - tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table[e.RowIndex,1]) == 2 ? rowHighlight : null; + tv.Style.RowColorGetter += (e) => Convert.ToInt32 (e.Table [e.RowIndex, 1]) == 2 ? rowHighlight : null; // private method for forcing the view to be focused/not focused var setFocusMethod = typeof (View).GetMethod ("SetHasFocus", BindingFlags.Instance | BindingFlags.NonPublic); @@ -944,7 +944,7 @@ public void TableView_ColorsTest_RowColorGetter (bool focused) // it no longer matches the RowColorGetter // delegate conditional ( which checks for // the value 2) - dt.Rows [0][1] = 5; + dt.Rows [0] [1] = 5; tv.Redraw (tv.Bounds); expected = @" @@ -1080,7 +1080,7 @@ private TableView SetUpMiniTable (out DataTable dt) dt.Columns.Add ("B"); dt.Rows.Add (1, 2); - tv.Table = new DataTableSource(dt); + tv.Table = new DataTableSource (dt); tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (0).MinWidth = 1; tv.Style.GetOrCreateColumnStyle (1).MaxWidth = 1; @@ -1144,7 +1144,7 @@ public void ScrollRight_SmoothScrolling () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -1205,7 +1205,7 @@ public void ScrollRight_WithoutSmoothScrolling () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -1264,7 +1264,7 @@ private TableView GetABCDEFTableView (out DataTable dt) dt.Columns.Add ("F"); dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); return tableView; } @@ -1315,7 +1315,7 @@ public void TestColumnStyle_AllColumnsVisibleFalse_BehavesAsTableNull () for (int i = 0; i < 6; i++) { tableView.Style.GetOrCreateColumnStyle (i).Visible = false; } - + tableView.LayoutSubviews (); // expect nothing to be rendered when all columns are invisible @@ -1814,7 +1814,7 @@ public void LongColumnTest () dt.Rows.Add (1, 2, new string ('a', 500)); dt.Rows.Add (1, 2, "aaa"); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); tableView.LayoutSubviews (); tableView.Redraw (tableView.Bounds); @@ -1957,7 +1957,7 @@ public void ScrollIndicators () dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); // select last visible column tableView.SelectedColumn = 2; // column C @@ -2022,7 +2022,7 @@ public void CellEventsBackgroundFill () dt.Rows.Add ("Hello", DBNull.Value, "f"); - tv.Table = new DataTableSource(dt); + tv.Table = new DataTableSource (dt); tv.NullSymbol = string.Empty; Application.Top.Add (tv); @@ -2334,7 +2334,7 @@ public static DataTableSource BuildTable (int cols, int rows, out DataTable dt) dt.Rows.Add (newRow); } - return new DataTableSource(dt); + return new DataTableSource (dt); } [Fact, AutoInitShutdown] @@ -2439,10 +2439,10 @@ public void Test_ScreenToCell_DataColumnOverload () // ---------------- X=1 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (1, 0, out col)); - Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); + Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in header row line (click in the horizontal line below header counts as click in header above - consistent with the column hit box) Assert.Null (tableView.ScreenToCell (1, 1, out col)); - Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); + Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in cell 0,0 Assert.Equal (new Point (0, 0), tableView.ScreenToCell (1, 2, out col)); Assert.Null (col); @@ -2456,7 +2456,7 @@ public void Test_ScreenToCell_DataColumnOverload () // ---------------- X=2 ----------------------- // click in header Assert.Null (tableView.ScreenToCell (2, 0, out col)); - Assert.Equal ("A", tableView.Table.ColumnNames[col.Value]); + Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (2, 1, out col)); Assert.Equal ("A", tableView.Table.ColumnNames [col.Value]); @@ -2476,7 +2476,7 @@ public void Test_ScreenToCell_DataColumnOverload () Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]); // click in header row line Assert.Null (tableView.ScreenToCell (3, 1, out col)); - Assert.Equal ("B", tableView.Table.ColumnNames[col.Value]); + Assert.Equal ("B", tableView.Table.ColumnNames [col.Value]); // click in cell 1,0 Assert.Equal (new Point (1, 0), tableView.ScreenToCell (3, 2, out col)); Assert.Null (col); @@ -2488,8 +2488,8 @@ public void Test_ScreenToCell_DataColumnOverload () Assert.Null (col); } - [Fact,AutoInitShutdown] - public void TestEnumerableDataSource_BasicTypes() + [Fact, AutoInitShutdown] + public void TestEnumerableDataSource_BasicTypes () { var tv = new TableView (); tv.ColorScheme = Colors.TopLevel; @@ -2526,7 +2526,7 @@ public void Test_CollectionNavigator () tv.Bounds = new Rect (0, 0, 50, 7); tv.Table = new EnumerableTableSource ( - new string [] { "fish", "troll", "trap","zoo" }, + new string [] { "fish", "troll", "trap", "zoo" }, new () { { "Name", (t)=>t}, { "EndsWith", (t)=>t.Last()} @@ -2554,7 +2554,7 @@ public void Test_CollectionNavigator () Assert.False (tv.HasFocus); // already on fish - tv.ProcessKey (new KeyEvent {Key = Key.f}); + tv.ProcessKey (new KeyEvent { Key = Key.f }); Assert.Equal (0, tv.SelectedRow); // not focused @@ -2579,7 +2579,7 @@ public void Test_CollectionNavigator () // move to troll tv.ProcessKey (new KeyEvent { Key = Key.t }); Assert.Equal (1, tv.SelectedRow); - + // move to trap tv.ProcessKey (new KeyEvent { Key = Key.t }); Assert.Equal (2, tv.SelectedRow); @@ -2632,7 +2632,7 @@ private TableView GetTwoRowSixColumnTable (out DataTable dt) dt.Rows.Add (1, 2, 3, 4, 5, 6); dt.Rows.Add (1, 2, 3, 4, 5, 6); - tableView.Table = new DataTableSource(dt); + tableView.Table = new DataTableSource (dt); return tableView; } }