Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for limiting max tokens in TokenizingTextBox #4163

Merged
merged 16 commits into from
Aug 13, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<RowDefinition/>
</Grid.RowDefinitions>
<StackPanel>
<TextBlock FontSize="32" Text="Select Actions"
<TextBlock FontSize="32" Text="Select up to 3 Actions"
shweaver-MSFT marked this conversation as resolved.
Show resolved Hide resolved
Margin="0,0,0,4"/>
<controls:TokenizingTextBox
x:Name="TokenBox"
Expand All @@ -39,7 +39,8 @@
MaxHeight="104"
HorizontalAlignment="Stretch"
TextMemberPath="Text"
TokenDelimiter=",">
TokenDelimiter=","
MaxTokens="3">
shweaver-MSFT marked this conversation as resolved.
Show resolved Hide resolved
<controls:TokenizingTextBox.SuggestedItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Toolkit.Uwp.UI.Controls
{
/// <summary>
/// Indicates how tokens are selected in the <see cref="TokenizingTextBox"/>.
/// </summary>
public enum TokenSelectionMode
{
/// <summary>
/// Only one token can be selected at a time. A new token should replace the active selection.
/// </summary>
Single,

/// <summary>
/// Multiple tokens can be selected at a time.
/// </summary>
Multiple,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,46 @@ private static void TextPropertyChanged(DependencyObject d, DependencyPropertyCh
typeof(TokenizingTextBox),
new PropertyMetadata(false));

/// <summary>
/// Identifies the <see cref="MaxTokens"/> property.
/// </summary>
public static readonly DependencyProperty MaxTokensProperty = DependencyProperty.Register(
nameof(MaxTokens),
typeof(int?),
typeof(TokenizingTextBox),
new PropertyMetadata(null, OnMaxTokensChanged));

private static void OnMaxTokensChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is TokenizingTextBox ttb && e.NewValue is int newMaxTokens)
{
var tokenCount = ttb.Items.Count;
if (tokenCount > newMaxTokens)
shweaver-MSFT marked this conversation as resolved.
Show resolved Hide resolved
{
int tokensToRemove = newMaxTokens - tokenCount;
var tokensRemoved = 0;

// Start at the end, remove any extra tokens.
for (var i = ttb._innerItemsSource.Count - 1; i >= 0; --i)
{
var item = ttb._innerItemsSource[i];
if (item is not ITokenStringContainer)
{
// Force remove the items. No warning and no option to cancel.
ttb._innerItemsSource.Remove(item);
ttb.TokenItemRemoved?.Invoke(ttb, item);

tokensRemoved++;
if (tokensRemoved == tokensToRemove)
{
break;
}
}
}
}
}
}

/// <summary>
/// Gets or sets the Style for the contained AutoSuggestBox template part.
/// </summary>
Expand Down Expand Up @@ -303,5 +343,14 @@ public string SelectedTokenText
return PrepareSelectionForClipboard();
}
}

/// <summary>
/// Gets or sets the maximum number of token results allowed at a time.
/// </summary>
public int? MaxTokens
shweaver-MSFT marked this conversation as resolved.
Show resolved Hide resolved
{
get => (int?)GetValue(MaxTokensProperty);
set => SetValue(MaxTokensProperty, value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,16 @@ private void ItemsSource_PropertyChanged(DependencyObject sender, DependencyProp
if (ItemsSource != null && ItemsSource.GetType() != typeof(InterspersedObservableCollection))
{
_innerItemsSource = new InterspersedObservableCollection(ItemsSource);

if (MaxTokens.HasValue && _innerItemsSource.ItemsSource.Count > MaxTokens)
{
// Reduce down to the max as necessary.
for (var i = _innerItemsSource.ItemsSource.Count; i > MaxTokens; --i)
{
_innerItemsSource.Remove(_innerItemsSource[i]);
}
}

_currentTextEdit = _lastTextEdit = new PretokenStringContainer(true);
_innerItemsSource.Insert(_innerItemsSource.Count, _currentTextEdit);
ItemsSource = _innerItemsSource;
Expand Down Expand Up @@ -278,18 +288,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

Expand Down Expand Up @@ -432,6 +440,12 @@ public async Task ClearAsync()

internal async Task AddTokenAsync(object data, bool? atEnd = null)
{
if (MaxTokens == 0)
{
// No tokens for you
return;
}

if (data is string str && TokenItemAdding != null)
{
var tiaea = new TokenItemAddingEventArgs(str);
Expand All @@ -451,6 +465,26 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null)
// If we've been typing in the last box, just add this to the end of our collection
if (atEnd == true || _currentTextEdit == _lastTextEdit)
{
if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
{
// Remove tokens from the end until below the max number.
for (var i = _innerItemsSource.Count - 2; i >= 0; --i)
{
var item = _innerItemsSource[i];
if (item is not ITokenStringContainer)
{
_innerItemsSource.Remove(item);
TokenItemRemoved?.Invoke(this, item);

// Keep going until we are below the max.
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
{
break;
}
}
}
}

_innerItemsSource.InsertAt(_innerItemsSource.Count - 1, data);
}
else
Expand All @@ -459,6 +493,26 @@ internal async Task AddTokenAsync(object data, bool? atEnd = null)
var edit = _currentTextEdit;
var index = _innerItemsSource.IndexOf(edit);

if (MaxTokens != null && _innerItemsSource.ItemsSource.Count >= MaxTokens)
{
// Find the next token and remove it, until below the max number of tokens.
for (var i = index; i < _innerItemsSource.Count; i++)
{
var item = _innerItemsSource[i];
if (item is not ITokenStringContainer)
{
_innerItemsSource.Remove(item);
TokenItemRemoved?.Invoke(this, item);

// Keep going until we are below the max.
if (_innerItemsSource.ItemsSource.Count < MaxTokens)
{
break;
}
}
}
}

// Insert our new data item at the location of our textbox
_innerItemsSource.InsertAt(index, data);

Expand Down