diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index b38058a507d..fca7b60e1de 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -30,8 +30,11 @@ - + + + + + + TokenDelimiter="," + MaximumTokens="3"> diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs index d6099b8596e..1a5a1e36b89 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.Properties.cs @@ -157,6 +157,37 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh typeof(TokenizingTextBox), new PropertyMetadata(false)); + /// + /// Identifies the property. + /// + public static readonly DependencyProperty MaximumTokensProperty = DependencyProperty.Register( + nameof(MaximumTokens), + typeof(int), + typeof(TokenizingTextBox), + new PropertyMetadata(null, OnMaximumTokensChanged)); + + private static void OnMaximumTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TokenizingTextBox ttb && ttb.ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && e.NewValue is int newMaxTokens) + { + var tokenCount = ttb._innerItemsSource.ItemsSource.Count; + if (tokenCount > 0 && tokenCount > newMaxTokens) + { + int tokensToRemove = tokenCount - Math.Max(newMaxTokens, 0); + + // Start at the end, remove any extra tokens. + for (var i = tokenCount; i > tokenCount - tokensToRemove; --i) + { + var token = ttb._innerItemsSource.ItemsSource[i - 1]; + + // Force remove the items. No warning and no option to cancel. + ttb._innerItemsSource.Remove(token); + ttb.TokenItemRemoved?.Invoke(ttb, token); + } + } + } + } + /// /// Gets or sets the Style for the contained AutoSuggestBox template part. /// @@ -303,5 +334,14 @@ public string SelectedTokenText return PrepareSelectionForClipboard(); } } + + /// + /// Gets or sets the maximum number of token results allowed at a time. + /// + public int MaximumTokens + { + get => (int)GetValue(MaximumTokensProperty); + set => SetValue(MaximumTokensProperty, value); + } } -} \ No newline at end of file +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs index 89474a89fc6..bf4d2567a0f 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBox.cs @@ -77,6 +77,17 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp if (ItemsSource != null && ItemsSource.GetType() != typeof(InterspersedObservableCollection)) { _innerItemsSource = new InterspersedObservableCollection(ItemsSource); + + if (ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && _innerItemsSource.ItemsSource.Count >= MaximumTokens) + { + // Reduce down to below the max as necessary. + var endCount = MaximumTokens > 0 ? MaximumTokens : 0; + for (var i = _innerItemsSource.ItemsSource.Count - 1; i >= endCount; --i) + { + _innerItemsSource.Remove(_innerItemsSource[i]); + } + } + _currentTextEdit = _lastTextEdit = new PretokenStringContainer(true); _innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit); ItemsSource = _innerItemsSource; @@ -278,18 +289,16 @@ void WaitForLoad(object s, RoutedEventArgs eargs) } else { - // TODO: It looks like we're setting selection and focus together on items? Not sure if that's what we want... - // If that's the case, don't think this code will ever be called? - - //// TODO: Behavior question: if no items selected (just focus) does it just go to our last active textbox? - //// Community voted that typing in the end box made sense - + // If no items are selected, send input to the last active string container. + // This code is only fires during an edgecase where an item is in the process of being deleted and the user inputs a character before the focus has been redirected to a string container. if (_innerItemsSource[_innerItemsSource.Count - 1] is ITokenStringContainer textToken) { var last = ContainerFromIndex(Items.Count - 1) as TokenizingTextBoxItem; // Should be our last text box - var position = last._autoSuggestTextBox.SelectionStart; - textToken.Text = last._autoSuggestTextBox.Text.Substring(0, position) + args.Character + - last._autoSuggestTextBox.Text.Substring(position); + var text = last._autoSuggestTextBox.Text; + var selectionStart = last._autoSuggestTextBox.SelectionStart; + var position = selectionStart > text.Length ? text.Length : selectionStart; + textToken.Text = text.Substring(0, position) + args.Character + + text.Substring(position); last._autoSuggestTextBox.SelectionStart = position + 1; // Set position to after our new character inserted @@ -432,6 +441,12 @@ public async Task ClearAsync() internal async Task AddTokenAsync(object data, bool? atEnd = null) { + if (ReadLocalValue(MaximumTokensProperty) != DependencyProperty.UnsetValue && (MaximumTokens <= 0 || MaximumTokens <= _innerItemsSource.ItemsSource.Count)) + { + // No tokens for you + return; + } + if (data is string str && TokenItemAdding != null) { var tiaea = new TokenItemAddingEventArgs(str); diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs index ebd64de2539..5a95f5b4b45 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.cs @@ -4,10 +4,12 @@ using Windows.Foundation; using Windows.System; +using Windows.UI; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; using Windows.UI.Xaml.Data; using Windows.UI.Xaml.Input; +using Windows.UI.Xaml.Media; namespace Microsoft.Toolkit.Uwp.UI.Controls { @@ -16,9 +18,11 @@ namespace Microsoft.Toolkit.Uwp.UI.Controls /// [System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.ReadabilityRules", "SA1124:Do not use regions", Justification = "Organization")] [TemplatePart(Name = PART_AutoSuggestBox, Type = typeof(AutoSuggestBox))] //// String case + [TemplatePart(Name = PART_TokensCounter, Type = typeof(TextBlock))] public partial class TokenizingTextBoxItem { private const string PART_AutoSuggestBox = "PART_AutoSuggestBox"; + private const string PART_TokensCounter = "PART_TokensCounter"; private AutoSuggestBox _autoSuggestBox; @@ -231,6 +235,8 @@ private void AutoSuggestBox_GotFocus(object sender, RoutedEventArgs e) #region Inner TextBox private void OnASBLoaded(object sender, RoutedEventArgs e) { + UpdateTokensCounter(this); + // Local function for Selection changed void AutoSuggestTextBox_SelectionChanged(object box, RoutedEventArgs args) { @@ -329,6 +335,44 @@ private void AutoSuggestTextBox_PreviewKeyDown(object sender, KeyRoutedEventArgs Owner.SelectAllTokensAndText(); } } + + private void UpdateTokensCounter(TokenizingTextBoxItem ttbi) + { + var maxTokensCounter = (TextBlock)_autoSuggestBox?.FindDescendant(PART_TokensCounter); + if (maxTokensCounter == null) + { + return; + } + + void OnTokenCountChanged(TokenizingTextBox ttb, object value = null) + { + var itemsSource = ttb.ItemsSource as InterspersedObservableCollection; + var currentTokens = itemsSource.ItemsSource.Count; + var maxTokens = ttb.MaximumTokens; + + maxTokensCounter.Text = $"{currentTokens}/{maxTokens}"; + maxTokensCounter.Visibility = Visibility.Visible; + + maxTokensCounter.Foreground = (currentTokens >= maxTokens) + ? new SolidColorBrush(Colors.Red) + : _autoSuggestBox.Foreground; + } + + ttbi.Owner.TokenItemAdded -= OnTokenCountChanged; + ttbi.Owner.TokenItemRemoved -= OnTokenCountChanged; + + if (Content is ITokenStringContainer str && str.IsLast && ttbi?.Owner != null && ttbi.Owner.ReadLocalValue(TokenizingTextBox.MaximumTokensProperty) != DependencyProperty.UnsetValue) + { + ttbi.Owner.TokenItemAdded += OnTokenCountChanged; + ttbi.Owner.TokenItemRemoved += OnTokenCountChanged; + OnTokenCountChanged(ttbi.Owner); + } + else + { + maxTokensCounter.Visibility = Visibility.Collapsed; + maxTokensCounter.Text = string.Empty; + } + } #endregion } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml index e0b9725492f..26dd6f69844 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Input/TokenizingTextBox/TokenizingTextBoxItem.AutoSuggestBox.xaml @@ -137,6 +137,7 @@ + @@ -176,7 +177,7 @@ ZoomMode="Disabled" /> + + +