diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml index d1b93c99b..94c89335c 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml +++ b/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml @@ -4,12 +4,13 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="clr-namespace:Wpf.Ui.Gallery.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" - xmlns:local="clr-namespace:Wpf.Ui.Gallery.Views.Pages.Text" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:text="clr-namespace:Wpf.Ui.Gallery.ViewModels.Pages.Text" + xmlns:text1="clr-namespace:Wpf.Ui.Gallery.Views.Pages.Text" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml" Title="AutoSuggestBoxPage" - d:DataContext="{d:DesignInstance local:AutoSuggestBoxPage, + d:DataContext="{d:DesignInstance text1:AutoSuggestBoxPage, IsDesignTimeCreatable=False}" d:DesignHeight="450" d:DesignWidth="800" @@ -27,6 +28,7 @@ x:Name="PageScrollViewer" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"> + @@ -38,17 +40,20 @@ + - + + + diff --git a/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml.cs b/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml.cs index 9e7d1fce0..36a12c926 100644 --- a/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml.cs +++ b/src/Wpf.Ui.Gallery/Views/Pages/Text/AutoSuggestBoxPage.xaml.cs @@ -3,7 +3,6 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. - using Wpf.Ui.Controls.Navigation; using Wpf.Ui.Gallery.ViewModels.Pages.Text; diff --git a/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml b/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml index 1fe7ea3e8..349a749f4 100644 --- a/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml +++ b/src/Wpf.Ui.Gallery/Views/Windows/MainWindow.xaml @@ -20,6 +20,14 @@ WindowCornerPreference="Default" WindowStartupLocation="CenterScreen" mc:Ignorable="d"> + + + + + @@ -40,9 +48,8 @@ diff --git a/src/Wpf.Ui/Controls/AutoSuggestBox.cs b/src/Wpf.Ui/Controls/AutoSuggestBox.cs deleted file mode 100644 index 4357fe099..000000000 --- a/src/Wpf.Ui/Controls/AutoSuggestBox.cs +++ /dev/null @@ -1,353 +0,0 @@ -// This Source Code Form is subject to the terms of the MIT License. -// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. -// Copyright (C) Leszek Pomianowski and WPF UI Contributors. -// All Rights Reserved. - -using System; -using System.Collections; -using System.Collections.Generic; -using System.ComponentModel; -using System.Drawing; -using System.Windows; -using System.Windows.Controls; -using System.Windows.Controls.Primitives; -using System.Windows.Input; - -namespace Wpf.Ui.Controls; - -// TODO: Fix closing and loosing focus - -/// -/// Represents a text control that makes suggestions to users as they enter text using a keyboard. -/// -[ToolboxItem(true)] -[ToolboxBitmap(typeof(AutoSuggestBox), "AutoSuggestBox.bmp")] -[TemplatePart(Name = "PART_Popup", Type = typeof(System.Windows.Controls.Primitives.Popup))] -[TemplatePart(Name = "PART_SuggestionsPresenter", Type = typeof(System.Windows.Controls.ListView))] -public class AutoSuggestBox : Wpf.Ui.Controls.TextBox -{ - /// - /// The current text in used for validation purposes. - /// - private string _currentText = String.Empty; - - /// - /// Template element represented by the PART_Popup name. - /// - private const string ElementPopup = "PART_Popup"; - - /// - /// Template element represented by the PART_SuggestionsPresenter name. - /// - private const string ElementSuggestionsPresenter = "PART_SuggestionsPresenter"; - - /// - /// Popup with suggestions. - /// - protected Popup? Popup { get; private set; } - - /// - /// List of suggestions inside . - /// - protected ListView? SuggestionsPresenter { get; private set; } - - /// - /// Property for . - /// - public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), - typeof(object), typeof(AutoSuggestBox), - new PropertyMetadata(null!, OnItemsSourceChanged)); - - /// - /// Property for . - /// - public static readonly DependencyProperty FilteredItemsSourceProperty = DependencyProperty.Register(nameof(FilteredItemsSource), - typeof(object), typeof(AutoSuggestBox), - new PropertyMetadata(null!)); - - /// - /// Property for . - /// - public static readonly DependencyProperty IsSuggestionListOpenProperty = DependencyProperty.Register(nameof(IsSuggestionListOpen), - typeof(bool), typeof(AutoSuggestBox), - new PropertyMetadata(false)); - - /// - /// Property for . - /// - public static readonly DependencyProperty MaxDropDownHeightProperty = DependencyProperty.Register(nameof(MaxDropDownHeight), - typeof(double), typeof(AutoSuggestBox), - new PropertyMetadata(240d)); - - /// - /// Routed event for . - /// - public static readonly RoutedEvent QuerySubmittedEvent = EventManager.RegisterRoutedEvent( - nameof(QuerySubmitted), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoSuggestBox)); - - /// - /// Routed event for . - /// - public static readonly RoutedEvent SuggestionChosenEvent = EventManager.RegisterRoutedEvent( - nameof(SuggestionChosen), RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(AutoSuggestBox)); - - /// - /// does no accept returns. - /// - public new bool AcceptsReturn - { - get => false; - set => throw new NotImplementedException($"{typeof(AutoSuggestBox)} does not accept returns."); - } - - /// - /// does not accept changes to the number of lines. - /// - public new int MaxLines - { - get => 1; - set => throw new NotImplementedException($"{typeof(AutoSuggestBox)} does not accept changes to the number of lines."); - } - - /// - /// does not accept changes to the number of lines. - /// - public new int MinLines - { - get => 1; - set => throw new NotImplementedException($"{typeof(AutoSuggestBox)} does not accept changes to the number of lines."); - } - - /// - /// ItemsSource specifies a collection used to generate the list of suggestions - /// for . - /// - [Bindable(true)] - public object ItemsSource - { - get => GetValue(ItemsSourceProperty); - set - { - if (value == null) - ClearValue(ItemsSourceProperty); - else - SetValue(ItemsSourceProperty, value); - } - } - - /// - /// Filtered based on provided text. - /// - public object FilteredItemsSource - { - get => GetValue(FilteredItemsSourceProperty); - private set - { - if (value == null) - ClearValue(FilteredItemsSourceProperty); - else - SetValue(FilteredItemsSourceProperty, value); - } - } - - /// - /// Gets or sets a value representing whether the suggestion list should be opened. - /// - public bool IsSuggestionListOpen - { - get => (bool)GetValue(IsSuggestionListOpenProperty); - set => SetValue(IsSuggestionListOpenProperty, value); - } - - /// - /// Gets or sets the maximum height of the drop-down list with suggestions. - /// - public double MaxDropDownHeight - { - get => (double)GetValue(MaxDropDownHeightProperty); - set => SetValue(MaxDropDownHeightProperty, value); - } - - /// - /// Event occurs when a user commits a query string. - /// - public event RoutedEventHandler QuerySubmitted - { - add => AddHandler(QuerySubmittedEvent, value); - remove => RemoveHandler(QuerySubmittedEvent, value); - } - - /// - /// Event occurs when the user selects an item from the recommended ones. - /// - public event RoutedEventHandler SuggestionChosen - { - add => AddHandler(SuggestionChosenEvent, value); - remove => RemoveHandler(SuggestionChosenEvent, value); - } - - /// - /// Gets the suggested result that the user chose. - /// - public object? ChosenSuggestion { get; protected set; } = null; - - /// - /// Invoked whenever application code or an internal process, - /// such as a rebuilding layout pass, calls the ApplyTemplate method. - /// - public override void OnApplyTemplate() - { - base.OnApplyTemplate(); - - if (GetTemplateChild(ElementPopup) is Popup popup) - Popup = popup; - - if (GetTemplateChild(ElementSuggestionsPresenter) is ListView listView) - SuggestionsPresenter = listView; - - if (SuggestionsPresenter == null!) - return; - - SuggestionsPresenter.SelectionChanged += OnSuggestionsPresenterSelectionChanged; - SuggestionsPresenter.LostFocus += OnSuggestionsPresenterLostFocus; - } - - /// - protected override void OnTextChanged(TextChangedEventArgs e) - { - base.OnTextChanged(e); - - if (ItemsSource is not ICollection itemsSourceCollection) - return; - - var newText = Text; - - if (_currentText == newText) - return; - - _currentText = newText; - - if (String.IsNullOrEmpty(newText)) - { - FilteredItemsSource = ItemsSource; - } - else - { - var formattedNewText = newText.ToLower(); - - var filteredCollection = new List(); - - foreach (var collectionItem in itemsSourceCollection) - if ((collectionItem?.ToString()?.ToLower() ?? String.Empty).Contains(formattedNewText) && collectionItem != null) - filteredCollection.Add(collectionItem); - - FilteredItemsSource = filteredCollection; - } - - OnQuerySubmitted(); - - IsSuggestionListOpen = true; - } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - if (e.Key == Key.Down && Popup != null && SuggestionsPresenter != null && Popup.IsOpen) - { - SuggestionsPresenter.Focus(); - - e.Handled = true; - - return; - } - - base.OnKeyDown(e); - } - - /// - protected override void OnLostFocus(RoutedEventArgs e) - { - IsSuggestionListOpen = false; - - base.OnLostFocus(e); - } - - /// - protected override void OnClearButtonClick() - { - base.OnClearButtonClick(); - - // TODO: Fix clearing search results - FilteredItemsSource = ItemsSource; - ChosenSuggestion = null; - } - - /// - /// This virtual method is called after presenter containing suggestion loses focus. - /// - protected virtual void OnSuggestionsPresenterLostFocus(object sender, RoutedEventArgs e) - { - if (!IsFocused) - IsSuggestionListOpen = false; - } - - /// - /// This virtual method is called after one of the suggestion is selected. - /// - protected virtual void OnSuggestionsPresenterSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is not ListView listView) - return; - - var selected = listView.SelectedItem; - - listView.UnselectAll(); - - ChosenSuggestion = selected ?? null; - var chosenSuggestionString = selected?.ToString() ?? String.Empty; - - Text = chosenSuggestionString; - CaretIndex = chosenSuggestionString.Length; - IsSuggestionListOpen = false; - - Focus(); - - OnSuggestionChosen(); - } - - /// - /// This virtual method is called after submitting a query. - /// - protected virtual void OnQuerySubmitted() - { - var newEvent = new RoutedEventArgs(QuerySubmittedEvent, this); - RaiseEvent(newEvent); - } - - /// - /// This virtual method is called after selecting a suggestion. - /// - protected virtual void OnSuggestionChosen() - { - var newEvent = new RoutedEventArgs(SuggestionChosenEvent, this); - RaiseEvent(newEvent); - } - - /// - /// This virtual method is called after is changed. - /// - protected virtual void OnItemsSourceChanged(IEnumerable itemsSource) - { - FilteredItemsSource = itemsSource; - } - - private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is not AutoSuggestBox autoSuggestBox) - return; - - if (e.NewValue is IEnumerable itemSource) - autoSuggestBox.OnItemsSourceChanged(itemSource); - } -} - diff --git a/src/Wpf.Ui/Controls/AutoSuggestBox.bmp b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBox.bmp similarity index 100% rename from src/Wpf.Ui/Controls/AutoSuggestBox.bmp rename to src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBox.bmp diff --git a/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBox.cs b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBox.cs new file mode 100644 index 000000000..6a48856ec --- /dev/null +++ b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBox.cs @@ -0,0 +1,515 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Drawing; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Input; +using System.Windows.Interop; +using Wpf.Ui.Common; +using Wpf.Ui.Interop; + +namespace Wpf.Ui.Controls.AutoSuggestBoxControl; + +[ToolboxItem(true)] +[ToolboxBitmap(typeof(AutoSuggestBox), "AutoSuggestBox.bmp")] +[TemplatePart(Name = ElementTextBox, Type = typeof(TextBox))] +[TemplatePart(Name = ElementSuggestionsPopup, Type = typeof(Popup))] +[TemplatePart(Name = ElementSuggestionsList, Type = typeof(ListView))] +public class AutoSuggestBox : System.Windows.Controls.ItemsControl +{ + protected const string ElementTextBox = "PART_TextBox"; + protected const string ElementSuggestionsPopup = "PART_SuggestionsPopup"; + protected const string ElementSuggestionsList = "PART_SuggestionsList"; + + #region Static properties + + /// + /// Property for . + /// + public static readonly DependencyProperty OriginalItemsSourceProperty = + DependencyProperty.Register(nameof(OriginalItemsSource), typeof(IList), typeof(AutoSuggestBox), + new PropertyMetadata(Array.Empty())); + + /// + /// Property for . + /// + public static readonly DependencyProperty IsSuggestionListOpenProperty = + DependencyProperty.Register(nameof(IsSuggestionListOpen), typeof(bool), typeof(AutoSuggestBox), + new PropertyMetadata(false)); + + /// + /// Property for . + /// + public static readonly DependencyProperty TextProperty = DependencyProperty.Register(nameof(Text), typeof(string), typeof(AutoSuggestBox), + new PropertyMetadata(string.Empty, TextPropertyChangedCallback)); + + /// + /// Property for . + /// + public static readonly DependencyProperty PlaceholderTextProperty = DependencyProperty.Register(nameof(PlaceholderText), typeof(string), typeof(AutoSuggestBox), + new PropertyMetadata(string.Empty)); + + /// + /// Property for . + /// + public static readonly DependencyProperty UpdateTextOnSelectProperty = DependencyProperty.Register(nameof(UpdateTextOnSelect), typeof(bool), typeof(AutoSuggestBox), + new PropertyMetadata(true)); + + /// + /// Property for . + /// + public static readonly DependencyProperty MaxSuggestionListHeightProperty = DependencyProperty.Register(nameof(MaxSuggestionListHeight), typeof(double), typeof(AutoSuggestBox), + new PropertyMetadata(0d)); + + /// + /// Property for . + /// + public static readonly DependencyProperty IconProperty = DependencyProperty.Register(nameof(Icon), typeof(SymbolRegular), typeof(AutoSuggestBox), + new PropertyMetadata(SymbolRegular.Empty)); + + /// + /// Property for . + /// + public static readonly DependencyProperty FocusCommandProperty = + DependencyProperty.Register(nameof(FocusCommand), typeof(ICommand), typeof(AutoSuggestBox), + new PropertyMetadata(null)); + + #endregion + + #region Properties + + /// + /// Set your items here if you want to use the default filtering + /// + public IList OriginalItemsSource + { + get => (IList)GetValue(OriginalItemsSourceProperty); + set => SetValue(OriginalItemsSourceProperty, value); + } + + /// + /// Gets or sets a Boolean value indicating whether the drop-down portion of the is open. + /// + public bool IsSuggestionListOpen + { + get => (bool)GetValue(IsSuggestionListOpenProperty); + set => SetValue(IsSuggestionListOpenProperty, value); + } + + /// + /// Gets or sets the text that is shown in the control + /// + /// + /// This property is not typically set in XAML. + /// + public string Text + { + get => (string)GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + /// + /// Gets or sets the placeholder text to be displayed in the control. + /// + /// + /// The placeholder text to be displayed in the control. The default is an empty string. + /// + public string PlaceholderText + { + get => (string)GetValue(PlaceholderTextProperty); + set => SetValue(PlaceholderTextProperty, value); + } + + /// + /// Gets or set the maximum height for the drop-down portion of the control. + /// + public double MaxSuggestionListHeight + { + get => (double)GetValue(MaxSuggestionListHeightProperty); + set => SetValue(MaxSuggestionListHeightProperty, value); + } + + /// + /// Gets or sets a value indicating whether items in the view will trigger an update of the editable text part of the when clicked. + /// + public bool UpdateTextOnSelect + { + get => (bool)GetValue(UpdateTextOnSelectProperty); + set => SetValue(UpdateTextOnSelectProperty, value); + } + + /// + /// Gets or sets displayed . + /// + public SymbolRegular Icon + { + get => (SymbolRegular)GetValue(IconProperty); + set => SetValue(IconProperty, value); + } + + /// + /// Used for focusing control + /// + public ICommand FocusCommand => (ICommand)GetValue(FocusCommandProperty); + + #endregion + + #region Events + + /// + /// Routed event for . + /// + public static readonly RoutedEvent QuerySubmittedEvent = EventManager.RegisterRoutedEvent( + nameof(QuerySubmitted), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(AutoSuggestBox)); + + /// + /// Routed event for . + /// + public static readonly RoutedEvent SuggestionChosenEvent = EventManager.RegisterRoutedEvent( + nameof(SuggestionChosen), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(AutoSuggestBox)); + + /// + /// Routed event for . + /// + public static readonly RoutedEvent TextChangedEvent = EventManager.RegisterRoutedEvent( + nameof(TextChanged), RoutingStrategy.Bubble, typeof(TypedEventHandler), typeof(AutoSuggestBox)); + + /// + /// Occurs when the user submits a search query. + /// + public event TypedEventHandler QuerySubmitted + { + add => AddHandler(QuerySubmittedEvent, value); + remove => RemoveHandler(QuerySubmittedEvent, value); + } + + /// + /// Event occurs when the user selects an item from the recommended ones. + /// + public event TypedEventHandler SuggestionChosen + { + add => AddHandler(SuggestionChosenEvent, value); + remove => RemoveHandler(SuggestionChosenEvent, value); + } + + /// + /// Raised after the text content of the editable control component is updated. + /// + public event TypedEventHandler TextChanged + { + add => AddHandler(TextChangedEvent, value); + remove => RemoveHandler(TextChangedEvent, value); + } + + #endregion + + protected TextBox TextBox = null!; + protected Popup SuggestionsPopup = null!; + protected ListView SuggestionsList = null!; + + private bool _changingTextAfterSuggestionChosen; + private bool _isChangedTextOutSideOfTextBox; + + private object? _selectedItem; + + public AutoSuggestBox() + { + Unloaded += static (sender, _) => + { + var self = (AutoSuggestBox)sender; + + self.ReleaseTemplateResources(); + }; + + SetValue(FocusCommandProperty, new RelayCommand(_ => Focus())); + } + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + TextBox = GetTemplateChild(ElementTextBox); + SuggestionsPopup = GetTemplateChild(ElementSuggestionsPopup); + SuggestionsList = GetTemplateChild(ElementSuggestionsList); + + TextBox.PreviewKeyDown += TextBoxOnPreviewKeyDown; + TextBox.TextChanged += TextBoxOnTextChanged; + TextBox.LostKeyboardFocus += TextBoxOnLostKeyboardFocus; + + SuggestionsList.SelectionChanged += SuggestionsListOnSelectionChanged; + SuggestionsList.PreviewKeyDown += SuggestionsListOnPreviewKeyDown; + SuggestionsList.LostKeyboardFocus += SuggestionsListOnLostKeyboardFocus; + SuggestionsList.PreviewMouseLeftButtonUp += SuggestionsListOnPreviewMouseLeftButtonUp; + + var hwnd = (HwndSource)PresentationSource.FromVisual(this)!; + hwnd.AddHook(Hook); + } + + /// + public new void Focus() + { + TextBox.Focus(); + } + + protected T GetTemplateChild(string name) where T : DependencyObject + { + if (GetTemplateChild(name) is not T dependencyObject) + throw new ArgumentNullException(name); + + return dependencyObject; + } + + protected virtual void ReleaseTemplateResources() + { + TextBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown; + TextBox.TextChanged -= TextBoxOnTextChanged; + TextBox.LostKeyboardFocus -= TextBoxOnLostKeyboardFocus; + + SuggestionsList.SelectionChanged -= SuggestionsListOnSelectionChanged; + SuggestionsList.PreviewKeyDown -= SuggestionsListOnPreviewKeyDown; + SuggestionsList.LostKeyboardFocus -= SuggestionsListOnLostKeyboardFocus; + SuggestionsList.PreviewMouseLeftButtonUp -= SuggestionsListOnPreviewMouseLeftButtonUp; + + if (PresentationSource.FromVisual(this) is HwndSource source) + source.RemoveHook(Hook); + } + + #region Events + + /// + /// Method for . + /// + /// + protected virtual void OnQuerySubmitted(string queryText) + { + var args = new AutoSuggestBoxQuerySubmittedEventArgs(QuerySubmittedEvent, this) + { + QueryText = queryText + }; + + RaiseEvent(args); + } + + /// + /// Method for . + /// + /// + protected virtual void OnSuggestionChosen(object selectedItem) + { + var args = new AutoSuggestBoxSuggestionChosenEventArgs(SuggestionChosenEvent, this) + { + SelectedItem = selectedItem + }; + + RaiseEvent(args); + + if (UpdateTextOnSelect && !args.Handled) + UpdateTexBoxTextAfterSelection(selectedItem); + } + + /// + /// Method for . + /// + /// + /// + protected virtual void OnTextChanged(AutoSuggestionBoxTextChangeReason reason, string text) + { + var args = new AutoSuggestBoxTextChangedEventArgs(TextChangedEvent, this) + { + Reason = reason, + Text = text + }; + + RaiseEvent(args); + + if (args is { Handled: false, Reason: AutoSuggestionBoxTextChangeReason.UserInput }) + DefaultFiltering(text); + } + + #endregion + + #region TextBox events + + private void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is Key.Escape) + { + IsSuggestionListOpen = false; + return; + } + + if (e.Key is Key.Enter) + { + IsSuggestionListOpen = false; + OnQuerySubmitted(TextBox.Text); + return; + } + + if (e.Key is not Key.Down || !IsSuggestionListOpen) + return; + + SuggestionsList.Focus(); + } + + private void TextBoxOnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (e.NewFocus is ListView) + return; + + IsSuggestionListOpen = false; + } + + private void TextBoxOnTextChanged(object sender, TextChangedEventArgs e) + { + var changeReason = AutoSuggestionBoxTextChangeReason.UserInput; + + if (_changingTextAfterSuggestionChosen) + changeReason = AutoSuggestionBoxTextChangeReason.SuggestionChosen; + + if (_isChangedTextOutSideOfTextBox) + changeReason = AutoSuggestionBoxTextChangeReason.ProgrammaticChange; + + OnTextChanged(changeReason, TextBox.Text); + + SuggestionsList.SelectedItem = null; + + if (changeReason is not AutoSuggestionBoxTextChangeReason.UserInput) + return; + + IsSuggestionListOpen = true; + } + + #endregion + + #region SuggestionsList events + + private void SuggestionsListOnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) + { + if (e.NewFocus is ListViewItem) + return; + + IsSuggestionListOpen = false; + } + + private void SuggestionsListOnPreviewKeyDown(object sender, KeyEventArgs e) + { + if (e.Key is not Key.Enter) + return; + + IsSuggestionListOpen = false; + OnSelectedChanged(SuggestionsList.SelectedItem); + } + + private void SuggestionsListOnPreviewMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + if (SuggestionsList.SelectedItem is not null) + return; + + IsSuggestionListOpen = false; + + if (_selectedItem is not null) + OnSuggestionChosen(_selectedItem); + } + + private void SuggestionsListOnSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (SuggestionsList.SelectedItem is null) + return; + + OnSelectedChanged(SuggestionsList.SelectedItem); + } + + #endregion + + private IntPtr Hook(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) + { + if (!IsSuggestionListOpen) + return IntPtr.Zero; + + var message = (User32.WM)msg; + + if (message is User32.WM.NCACTIVATE or User32.WM.WINDOWPOSCHANGED) + IsSuggestionListOpen = false; + + return IntPtr.Zero; + } + + private void OnSelectedChanged(object selectedObj) + { + OnSuggestionChosen(selectedObj); + + _selectedItem = selectedObj; + } + + private void UpdateTexBoxTextAfterSelection(object selectedObj) + { + _changingTextAfterSuggestionChosen = true; + + TextBox.Text = GetStringFromObj(selectedObj); + _changingTextAfterSuggestionChosen = false; + } + + private void DefaultFiltering(string text) + { + if (string.IsNullOrEmpty(text)) + { + ItemsSource = OriginalItemsSource; + return; + } + + var suitableItems = new List(); + var splitText = text.ToLower().Split(' '); + + for (var i = 0; i < OriginalItemsSource.Count; i++) + { + var item = OriginalItemsSource[i]; + var itemText = GetStringFromObj(item); + + var found = splitText.All(key=> itemText.ToLower().Contains(key)); + + if (found) + suitableItems.Add(item); + } + + ItemsSource = suitableItems; + } + + private string GetStringFromObj(object obj) + { + string text = string.Empty; + + if (!string.IsNullOrEmpty(DisplayMemberPath)) + { + //Maybe it needs some optimization? + if (obj.GetType().GetProperty(DisplayMemberPath)?.GetValue(obj) is string value) + text = value; + } + + if (string.IsNullOrEmpty(text)) + text = obj as string ?? obj.ToString(); + + return text; + } + + private static void TextPropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var self = (AutoSuggestBox)d; + var newText = (string)e.NewValue; + + if (self.TextBox.Text == newText) + return; + + self._isChangedTextOutSideOfTextBox = true; + self.TextBox.Text = newText; + self._isChangedTextOutSideOfTextBox = false; + } +} diff --git a/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxQuerySubmittedEventArgs.cs b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxQuerySubmittedEventArgs.cs new file mode 100644 index 000000000..f1d13361e --- /dev/null +++ b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxQuerySubmittedEventArgs.cs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; + +namespace Wpf.Ui.Controls.AutoSuggestBoxControl; + +/// +/// Provides event data for the event. +/// +public sealed class AutoSuggestBoxQuerySubmittedEventArgs : RoutedEventArgs +{ + public AutoSuggestBoxQuerySubmittedEventArgs(RoutedEvent eventArgs, object sender) : base(eventArgs, sender) + { + + } + + public required string QueryText { get; init; } +} diff --git a/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxSuggestionChosenEventArgs.cs b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxSuggestionChosenEventArgs.cs new file mode 100644 index 000000000..dc42443e8 --- /dev/null +++ b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxSuggestionChosenEventArgs.cs @@ -0,0 +1,21 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; + +namespace Wpf.Ui.Controls.AutoSuggestBoxControl; + +/// +/// Provides data for the event. +/// +public sealed class AutoSuggestBoxSuggestionChosenEventArgs : RoutedEventArgs +{ + public AutoSuggestBoxSuggestionChosenEventArgs(RoutedEvent eventArgs, object sender) : base(eventArgs, sender) + { + + } + + public required object SelectedItem { get; init; } +} diff --git a/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxTextChangedEventArgs.cs b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxTextChangedEventArgs.cs new file mode 100644 index 000000000..437ee42a6 --- /dev/null +++ b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestBoxTextChangedEventArgs.cs @@ -0,0 +1,22 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +using System.Windows; + +namespace Wpf.Ui.Controls.AutoSuggestBoxControl; + +/// +/// Provides data for the event. +/// +public sealed class AutoSuggestBoxTextChangedEventArgs : RoutedEventArgs +{ + public AutoSuggestBoxTextChangedEventArgs(RoutedEvent eventArgs, object sender) : base(eventArgs, sender) + { + + } + + public required string Text { get; init; } + public required AutoSuggestionBoxTextChangeReason Reason { get; init; } +} diff --git a/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestionBoxTextChangeReason.cs b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestionBoxTextChangeReason.cs new file mode 100644 index 000000000..772a32653 --- /dev/null +++ b/src/Wpf.Ui/Controls/AutoSuggestBoxControl/AutoSuggestionBoxTextChangeReason.cs @@ -0,0 +1,27 @@ +// This Source Code Form is subject to the terms of the MIT License. +// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT. +// Copyright (C) Leszek Pomianowski and WPF UI Contributors. +// All Rights Reserved. + +namespace Wpf.Ui.Controls.AutoSuggestBoxControl; + +/// +/// Provides data for the event. +/// +public enum AutoSuggestionBoxTextChangeReason +{ + /// + /// The user edited the text. + /// + UserInput = 0, + + /// + /// The text was changed via code. + /// + ProgrammaticChange = 1, + + /// + /// The user selected one of the items in the auto-suggestion box. + /// + SuggestionChosen = 2, +} diff --git a/src/Wpf.Ui/Controls/Navigation/INavigationView.cs b/src/Wpf.Ui/Controls/Navigation/INavigationView.cs index 18561cf49..ca10dd679 100644 --- a/src/Wpf.Ui/Controls/Navigation/INavigationView.cs +++ b/src/Wpf.Ui/Controls/Navigation/INavigationView.cs @@ -13,6 +13,7 @@ using Wpf.Ui.Animations; using Wpf.Ui.Common; using Wpf.Ui.Contracts; +using Wpf.Ui.Controls.AutoSuggestBoxControl; using Wpf.Ui.Controls.TitleBarControl; namespace Wpf.Ui.Controls.Navigation; diff --git a/src/Wpf.Ui/Controls/Navigation/NavigationView.Base.cs b/src/Wpf.Ui/Controls/Navigation/NavigationView.Base.cs index 735169d40..53ec73c4f 100644 --- a/src/Wpf.Ui/Controls/Navigation/NavigationView.Base.cs +++ b/src/Wpf.Ui/Controls/Navigation/NavigationView.Base.cs @@ -13,10 +13,12 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; +using System.Linq; using System.Windows; using System.Windows.Input; using Wpf.Ui.Common; using Wpf.Ui.Controls.BreadcrumbControl; +using Wpf.Ui.Controls.AutoSuggestBoxControl; namespace Wpf.Ui.Controls.Navigation; @@ -72,8 +74,9 @@ protected override void OnInitialized(EventArgs e) if (AutoSuggestBox is not null) { - AutoSuggestBox.ItemsSource = _autoSuggestBoxItems; + AutoSuggestBox.OriginalItemsSource = _autoSuggestBoxItems; AutoSuggestBox.SuggestionChosen += AutoSuggestBoxOnSuggestionChosen; + AutoSuggestBox.QuerySubmitted += AutoSuggestBoxOnQuerySubmitted; } InvalidateArrange(); @@ -109,7 +112,10 @@ protected virtual void OnUnloaded(object sender, RoutedEventArgs e) ClearJournal(); if (AutoSuggestBox is not null) + { AutoSuggestBox.SuggestionChosen -= AutoSuggestBoxOnSuggestionChosen; + AutoSuggestBox.QuerySubmitted -= AutoSuggestBoxOnQuerySubmitted; + } if (Header is BreadcrumbBar breadcrumbBar) { @@ -206,12 +212,12 @@ private void UpdateAutoSuggestBoxSuggestions() /// /// Navigate to the page after its name is selected in . /// - private void AutoSuggestBoxOnSuggestionChosen(object sender, RoutedEventArgs e) + private void AutoSuggestBoxOnSuggestionChosen(AutoSuggestBox sender, AutoSuggestBoxSuggestionChosenEventArgs args) { - if (sender is not AutoSuggestBox { ChosenSuggestion: string selectedSuggestBoxItem }) + if (sender.IsSuggestionListOpen) return; - if (string.IsNullOrEmpty(selectedSuggestBoxItem)) + if (args.SelectedItem is not string selectedSuggestBoxItem) return; if (NavigateToMenuItemFromAutoSuggestBox(MenuItems, selectedSuggestBoxItem)) @@ -220,6 +226,36 @@ private void AutoSuggestBoxOnSuggestionChosen(object sender, RoutedEventArgs e) NavigateToMenuItemFromAutoSuggestBox(FooterMenuItems, selectedSuggestBoxItem); } + private void AutoSuggestBoxOnQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + var suggestions = new List(); + var querySplit = args.QueryText.Split(' '); + + foreach (var item in _autoSuggestBoxItems) + { + bool isMatch = true; + + foreach (string queryToken in querySplit) + { + if (item.IndexOf(queryToken, StringComparison.CurrentCultureIgnoreCase) < 0) + isMatch = false; + } + + if (isMatch) + suggestions.Add(item); + } + + if (suggestions.Count <= 0) + return; + + var element = suggestions.First(); + + if (NavigateToMenuItemFromAutoSuggestBox(MenuItems, element)) + return; + + NavigateToMenuItemFromAutoSuggestBox(FooterMenuItems, element); + } + protected virtual void AddItemsToDictionaries(IList list) { for (var i = 0; i < list.Count; i++) diff --git a/src/Wpf.Ui/Controls/Navigation/NavigationView.Properties.cs b/src/Wpf.Ui/Controls/Navigation/NavigationView.Properties.cs index 722117f9a..ab5671e97 100644 --- a/src/Wpf.Ui/Controls/Navigation/NavigationView.Properties.cs +++ b/src/Wpf.Ui/Controls/Navigation/NavigationView.Properties.cs @@ -13,6 +13,7 @@ using System.Windows.Controls; using Wpf.Ui.Animations; using Wpf.Ui.Controls.TitleBarControl; +using Wpf.Ui.Controls.AutoSuggestBoxControl; namespace Wpf.Ui.Controls.Navigation; diff --git a/src/Wpf.Ui/Properties/AssemblyInfo.cs b/src/Wpf.Ui/Properties/AssemblyInfo.cs index cf644e052..3a25c5186 100644 --- a/src/Wpf.Ui/Properties/AssemblyInfo.cs +++ b/src/Wpf.Ui/Properties/AssemblyInfo.cs @@ -19,6 +19,7 @@ [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Converters")] [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.Navigation")] [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.Window")] +[assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.AutoSuggestBoxControl")] [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.TreeGridControl")] [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.VirtualizingControls")] [assembly: XmlnsDefinition("http://schemas.lepo.co/wpfui/2022/xaml", "Wpf.Ui.Controls.TitleBarControl")] diff --git a/src/Wpf.Ui/Styles/Controls/AutoSuggestBox.xaml b/src/Wpf.Ui/Styles/Controls/AutoSuggestBox.xaml index 2b342ba61..055695314 100644 --- a/src/Wpf.Ui/Styles/Controls/AutoSuggestBox.xaml +++ b/src/Wpf.Ui/Styles/Controls/AutoSuggestBox.xaml @@ -1,16 +1,7 @@ - - - @@ -23,13 +14,14 @@ 24 14 - - -