diff --git a/.gitremotes b/.gitremotes index e2f42ce..be196eb 100644 --- a/.gitremotes +++ b/.gitremotes @@ -1,10 +1,10 @@ -FSI.Lib //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/fsi.lib.git (fetch) -FSI.Lib //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/fsi.lib.git (push) -NHotkey //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/nhotkey.git (fetch) -NHotkey //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/nhotkey.git (push) -NotifyIconWpf //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/notifyiconwpf.git (fetch) -NotifyIconWpf //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/notifyiconwpf.git (push) -RadialMenu //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/radialmenu.git (fetch) -RadialMenu //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/radialmenu.git (push) -origin //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/fsi.tools.git (fetch) -origin //fondium.org/DESI$/AUG_Abteilung/Betriebstechnik/50_I&R/01_I&R Giesserei/100_Sicherung/E/99_Repositories/fsi/fsi.tools.git (push) +FSI.Lib r:/fsi/fsi.lib.git (fetch) +FSI.Lib r:/fsi/fsi.lib.git (push) +NHotkey r:/fsi/nhotkey.git (fetch) +NHotkey r:/fsi/nhotkey.git (push) +NotifyIconWpf r:/fsi/notifyiconwpf.git (fetch) +NotifyIconWpf r:/fsi/notifyiconwpf.git (push) +RadialMenu r:/fsi/radialmenu.git (fetch) +RadialMenu r:/fsi/radialmenu.git (push) +origin r:/fsi/fsi.bt.tools.git (fetch) +origin r:/fsi/fsi.bt.tools.git (push) diff --git a/AutoCompleteTextBox/AutoCompleteTextBox.csproj b/AutoCompleteTextBox/AutoCompleteTextBox.csproj new file mode 100644 index 0000000..4b2f404 --- /dev/null +++ b/AutoCompleteTextBox/AutoCompleteTextBox.csproj @@ -0,0 +1,32 @@ + + + + false + net48;netcoreapp3.1;net6.0-windows + true + 1.6.0.0 + https://github.com/quicoli/WPF-AutoComplete-TextBox + + https://github.com/quicoli/WPF-AutoComplete-TextBox + wpf, autocomplete, usercontrol + https://github.com/quicoli/WPF-AutoComplete-TextBox/blob/develop/AutoCompleteTextBox/Logo/AutoCompleteTextBox.ico?raw=true + true + AutoCompleteTextBox.ico + + Better support for keyboard focus + + An auto complete textbox and combo box for WPF + AutoCompleteTextBox.png + README.md + + + + True + \ + + + True + \ + + + diff --git a/AutoCompleteTextBox/AutoCompleteTextBox.ico b/AutoCompleteTextBox/AutoCompleteTextBox.ico new file mode 100644 index 0000000..4cb9958 Binary files /dev/null and b/AutoCompleteTextBox/AutoCompleteTextBox.ico differ diff --git a/AutoCompleteTextBox/BindingEvaluator.cs b/AutoCompleteTextBox/BindingEvaluator.cs new file mode 100644 index 0000000..6b06a7c --- /dev/null +++ b/AutoCompleteTextBox/BindingEvaluator.cs @@ -0,0 +1,49 @@ +using System.Windows; +using System.Windows.Data; + +namespace AutoCompleteTextBox +{ + public class BindingEvaluator : FrameworkElement + { + + #region "Fields" + + + public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(string), typeof(BindingEvaluator), new FrameworkPropertyMetadata(string.Empty)); + + #endregion + + #region "Constructors" + + public BindingEvaluator(Binding binding) + { + ValueBinding = binding; + } + + #endregion + + #region "Properties" + + public string Value + { + get => (string)GetValue(ValueProperty); + + set => SetValue(ValueProperty, value); + } + + public Binding ValueBinding { get; set; } + + #endregion + + #region "Methods" + + public string Evaluate(object dataItem) + { + DataContext = dataItem; + SetBinding(ValueProperty, ValueBinding); + return Value; + } + + #endregion + } +} \ No newline at end of file diff --git a/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs b/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs new file mode 100644 index 0000000..2b65103 --- /dev/null +++ b/AutoCompleteTextBox/Editors/AutoCompleteComboBox.cs @@ -0,0 +1,612 @@ +using System; +using System.Collections; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace AutoCompleteTextBox.Editors +{ + [TemplatePart(Name = PartEditor, Type = typeof(TextBox))] + [TemplatePart(Name = PartPopup, Type = typeof(Popup))] + [TemplatePart(Name = PartSelector, Type = typeof(Selector))] + [TemplatePart(Name = PartExpander, Type = typeof(Expander))] + public class AutoCompleteComboBox : Control + { + + #region "Fields" + + public const string PartEditor = "PART_Editor"; + public const string PartPopup = "PART_Popup"; + + public const string PartSelector = "PART_Selector"; + public const string PartExpander = "PART_Expander"; + public static readonly DependencyProperty DelayProperty = DependencyProperty.Register("Delay", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(200)); + public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.Register("DisplayMember", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty)); + public static readonly DependencyProperty IconPlacementProperty = DependencyProperty.Register("IconPlacement", typeof(IconPlacement), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(IconPlacement.Left)); + public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty IconVisibilityProperty = DependencyProperty.Register("IconVisibility", typeof(Visibility), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(Visibility.Visible)); + public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register("ItemTemplateSelector", typeof(DataTemplateSelector), typeof(AutoCompleteComboBox)); + public static readonly DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty ProviderProperty = DependencyProperty.Register("Provider", typeof(IComboSuggestionProvider), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(null, OnSelectedItemChanged)); + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty, propertyChangedCallback: null, coerceValueCallback: null, isAnimationProhibited: false, defaultUpdateSourceTrigger: UpdateSourceTrigger.LostFocus, flags: FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + public static readonly DependencyProperty FilterProperty = DependencyProperty.Register("Filter", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty)); + public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register("MaxLength", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(0)); + public static readonly DependencyProperty CharacterCasingProperty = DependencyProperty.Register("CharacterCasing", typeof(CharacterCasing), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(CharacterCasing.Normal)); + public static readonly DependencyProperty MaxPopUpHeightProperty = DependencyProperty.Register("MaxPopUpHeight", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(600)); + public static readonly DependencyProperty MaxPopUpWidthProperty = DependencyProperty.Register("MaxPopUpWidth", typeof(int), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(2000)); + + public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(string), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(string.Empty)); + + public static readonly DependencyProperty SuggestionBackgroundProperty = DependencyProperty.Register("SuggestionBackground", typeof(Brush), typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(Brushes.White)); + private bool _isUpdatingText; + private bool _selectionCancelled; + + private SuggestionsAdapter _suggestionsAdapter; + + + #endregion + + #region "Constructors" + + static AutoCompleteComboBox() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoCompleteComboBox), new FrameworkPropertyMetadata(typeof(AutoCompleteComboBox))); + } + + #endregion + + #region "Properties" + + + public int MaxPopupHeight + { + get => (int)GetValue(MaxPopUpHeightProperty); + set => SetValue(MaxPopUpHeightProperty, value); + } + public int MaxPopupWidth + { + get => (int)GetValue(MaxPopUpWidthProperty); + set => SetValue(MaxPopUpWidthProperty, value); + } + + + public BindingEvaluator BindingEvaluator { get; set; } + + public CharacterCasing CharacterCasing + { + get => (CharacterCasing)GetValue(CharacterCasingProperty); + set => SetValue(CharacterCasingProperty, value); + } + + public int MaxLength + { + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + public int Delay + { + get => (int)GetValue(DelayProperty); + + set => SetValue(DelayProperty, value); + } + + public string DisplayMember + { + get => (string)GetValue(DisplayMemberProperty); + + set => SetValue(DisplayMemberProperty, value); + } + + public TextBox Editor { get; set; } + public Expander Expander { get; set; } + + public DispatcherTimer FetchTimer { get; set; } + + public string Filter + { + get => (string)GetValue(FilterProperty); + + set => SetValue(FilterProperty, value); + } + + public object Icon + { + get => GetValue(IconProperty); + + set => SetValue(IconProperty, value); + } + + public IconPlacement IconPlacement + { + get => (IconPlacement)GetValue(IconPlacementProperty); + + set => SetValue(IconPlacementProperty, value); + } + + public Visibility IconVisibility + { + get => (Visibility)GetValue(IconVisibilityProperty); + + set => SetValue(IconVisibilityProperty, value); + } + + public bool IsDropDownOpen + { + get => (bool)GetValue(IsDropDownOpenProperty); + + set + { + this.Expander.IsExpanded = value; + SetValue(IsDropDownOpenProperty, value); + } + } + + public bool IsLoading + { + get => (bool)GetValue(IsLoadingProperty); + + set => SetValue(IsLoadingProperty, value); + } + + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + + set => SetValue(IsReadOnlyProperty, value); + } + + public Selector ItemsSelector { get; set; } + + public DataTemplate ItemTemplate + { + get => (DataTemplate)GetValue(ItemTemplateProperty); + + set => SetValue(ItemTemplateProperty, value); + } + + public DataTemplateSelector ItemTemplateSelector + { + get => ((DataTemplateSelector)(GetValue(ItemTemplateSelectorProperty))); + set => SetValue(ItemTemplateSelectorProperty, value); + } + + public object LoadingContent + { + get => GetValue(LoadingContentProperty); + + set => SetValue(LoadingContentProperty, value); + } + + public Popup Popup { get; set; } + + public IComboSuggestionProvider Provider + { + get => (IComboSuggestionProvider)GetValue(ProviderProperty); + + set => SetValue(ProviderProperty, value); + } + + public object SelectedItem + { + get => GetValue(SelectedItemProperty); + + set => SetValue(SelectedItemProperty, value); + } + + public SelectionAdapter SelectionAdapter { get; set; } + + public string Text + { + get => (string)GetValue(TextProperty); + + set => SetValue(TextProperty, value); + } + + public string Watermark + { + get => (string)GetValue(WatermarkProperty); + + set => SetValue(WatermarkProperty, value); + } + public Brush SuggestionBackground + { + get => (Brush)GetValue(SuggestionBackgroundProperty); + + set => SetValue(SuggestionBackgroundProperty, value); + } + + #endregion + + #region "Methods" + + public static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + AutoCompleteComboBox act = null; + act = d as AutoCompleteComboBox; + if (act != null) + { + if (act.Editor != null & !act._isUpdatingText) + { + act._isUpdatingText = true; + act.Editor.Text = act.BindingEvaluator.Evaluate(e.NewValue); + act._isUpdatingText = false; + } + } + } + + private void ScrollToSelectedItem() + { + if (ItemsSelector is ListBox listBox && listBox.SelectedItem != null) + listBox.ScrollIntoView(listBox.SelectedItem); + } + + public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding){ + var res = base.SetBinding(dp, binding); + CheckForParentTextBindingChange(); + return res; + } + public new BindingExpressionBase SetBinding(DependencyProperty dp, String path) { + var res = base.SetBinding(dp, path); + CheckForParentTextBindingChange(); + return res; + } + public new void ClearValue(DependencyPropertyKey key) { + base.ClearValue(key); + CheckForParentTextBindingChange(); + } + public new void ClearValue(DependencyProperty dp) { + base.ClearValue(dp); + CheckForParentTextBindingChange(); + } + private void CheckForParentTextBindingChange(bool force=false) { + var CurrentBindingMode = BindingOperations.GetBinding(this, TextProperty)?.UpdateSourceTrigger ?? UpdateSourceTrigger.Default; + if (CurrentBindingMode != UpdateSourceTrigger.PropertyChanged)//preventing going any less frequent than property changed + CurrentBindingMode = UpdateSourceTrigger.Default; + + + if (CurrentBindingMode == CurrentTextboxTextBindingUpdateMode && force == false) + return; + var binding = new Binding { + Mode = BindingMode.TwoWay, + UpdateSourceTrigger = CurrentBindingMode, + Path = new PropertyPath(nameof(Text)), + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }; + CurrentTextboxTextBindingUpdateMode = CurrentBindingMode; + Editor?.SetBinding(TextBox.TextProperty, binding); + } + + private UpdateSourceTrigger CurrentTextboxTextBindingUpdateMode; + + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Editor = Template.FindName(PartEditor, this) as TextBox; + Popup = Template.FindName(PartPopup, this) as Popup; + ItemsSelector = Template.FindName(PartSelector, this) as Selector; + Expander = Template.FindName(PartExpander, this) as Expander; + + BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember)); + + if (Editor != null) + { + Editor.TextChanged += OnEditorTextChanged; + Editor.PreviewKeyDown += OnEditorKeyDown; + Editor.LostFocus += OnEditorLostFocus; + CheckForParentTextBindingChange(true); + + if (SelectedItem != null) + { + _isUpdatingText = true; + Editor.Text = BindingEvaluator.Evaluate(SelectedItem); + _isUpdatingText = false; + } + + } + if (Expander != null) + { + Expander.IsExpanded = false; + Expander.Collapsed += Expander_Expanded; + Expander.Expanded += Expander_Expanded; + } + + GotFocus += AutoCompleteComboBox_GotFocus; + + if (Popup != null) + { + Popup.StaysOpen = false; + Popup.Opened += OnPopupOpened; + Popup.Closed += OnPopupClosed; + } + if (ItemsSelector != null) + { + SelectionAdapter = new SelectionAdapter(ItemsSelector); + SelectionAdapter.Commit += OnSelectionAdapterCommit; + SelectionAdapter.Cancel += OnSelectionAdapterCancel; + SelectionAdapter.SelectionChanged += OnSelectionAdapterSelectionChanged; + ItemsSelector.PreviewMouseDown += ItemsSelector_PreviewMouseDown; + } + } + + private void Expander_Expanded(object sender, RoutedEventArgs e) + { + this.IsDropDownOpen = Expander.IsExpanded; + if (!this.IsDropDownOpen) + { + return; + } + if (_suggestionsAdapter == null) + { + _suggestionsAdapter = new SuggestionsAdapter(this); + } + if (SelectedItem != null || String.IsNullOrWhiteSpace(Editor.Text)) + _suggestionsAdapter.ShowFullCollection(); + + } + + private void ItemsSelector_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + if ((e.OriginalSource as FrameworkElement)?.DataContext == null) + return; + if (!ItemsSelector.Items.Contains(((FrameworkElement)e.OriginalSource)?.DataContext)) + return; + ItemsSelector.SelectedItem = ((FrameworkElement)e.OriginalSource)?.DataContext; + OnSelectionAdapterCommit(SelectionAdapter.EventCause.ItemClicked); + e.Handled = true; + } + private void AutoCompleteComboBox_GotFocus(object sender, RoutedEventArgs e) + { + Editor?.Focus(); + } + + private string GetDisplayText(object dataItem) + { + if (BindingEvaluator == null) + { + BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember)); + } + if (dataItem == null) + { + return string.Empty; + } + if (string.IsNullOrEmpty(DisplayMember)) + { + return dataItem.ToString(); + } + return BindingEvaluator.Evaluate(dataItem); + } + + private void OnEditorKeyDown(object sender, KeyEventArgs e) + { + if (SelectionAdapter != null) + { + if (IsDropDownOpen) + SelectionAdapter.HandleKeyDown(e); + else + IsDropDownOpen = e.Key == Key.Down || e.Key == Key.Up; + } + } + + private void OnEditorLostFocus(object sender, RoutedEventArgs e) + { + if (!IsKeyboardFocusWithin) + { + IsDropDownOpen = false; + } + } + + private void OnEditorTextChanged(object sender, TextChangedEventArgs e) + { + Text = Editor.Text; + if (_isUpdatingText) + return; + if (FetchTimer == null) + { + FetchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(Delay) }; + FetchTimer.Tick += OnFetchTimerTick; + } + FetchTimer.IsEnabled = false; + FetchTimer.Stop(); + SetSelectedItem(null); + if (Editor.Text.Length > 0) + { + FetchTimer.IsEnabled = true; + FetchTimer.Start(); + } + else + { + IsDropDownOpen = false; + } + } + + private void OnFetchTimerTick(object sender, EventArgs e) + { + FetchTimer.IsEnabled = false; + FetchTimer.Stop(); + if (Provider != null && ItemsSelector != null) + { + Filter = Editor.Text; + if (_suggestionsAdapter == null) + { + _suggestionsAdapter = new SuggestionsAdapter(this); + } + _suggestionsAdapter.GetSuggestions(Filter); + } + } + + private void OnPopupClosed(object sender, EventArgs e) + { + if (!_selectionCancelled) + { + OnSelectionAdapterCommit(SelectionAdapter.EventCause.PopupClosed); + } + } + + private void OnPopupOpened(object sender, EventArgs e) + { + _selectionCancelled = false; + ItemsSelector.SelectedItem = SelectedItem; + } + + public event EventHandler PreSelectionAdapterFinish; + private bool PreSelectionEventSomeoneHandled(SelectionAdapter.EventCause cause, bool is_cancel) { + if (PreSelectionAdapterFinish == null) + return false; + var args = new SelectionAdapter.PreSelectionAdapterFinishArgs { cause = cause, is_cancel = is_cancel }; + PreSelectionAdapterFinish?.Invoke(this, args); + return args.handled; + + } + private void OnSelectionAdapterCancel(SelectionAdapter.EventCause cause) + { + if (PreSelectionEventSomeoneHandled(cause, true)) + return; + _isUpdatingText = true; + Editor.Text = SelectedItem == null ? Filter : GetDisplayText(SelectedItem); + Editor.SelectionStart = Editor.Text.Length; + Editor.SelectionLength = 0; + _isUpdatingText = false; + IsDropDownOpen = false; + _selectionCancelled = true; + } + + private void OnSelectionAdapterCommit(SelectionAdapter.EventCause cause) + { + if (PreSelectionEventSomeoneHandled(cause, false)) + return; + + if (ItemsSelector.SelectedItem != null) + { + SelectedItem = ItemsSelector.SelectedItem; + _isUpdatingText = true; + Editor.Text = GetDisplayText(ItemsSelector.SelectedItem); + SetSelectedItem(ItemsSelector.SelectedItem); + _isUpdatingText = false; + IsDropDownOpen = false; + } + } + + private void OnSelectionAdapterSelectionChanged() + { + _isUpdatingText = true; + Editor.Text = ItemsSelector.SelectedItem == null ? Filter : GetDisplayText(ItemsSelector.SelectedItem); + Editor.SelectionStart = Editor.Text.Length; + Editor.SelectionLength = 0; + ScrollToSelectedItem(); + _isUpdatingText = false; + } + + private void SetSelectedItem(object item) + { + _isUpdatingText = true; + SelectedItem = item; + _isUpdatingText = false; + } + #endregion + + #region "Nested Types" + + private class SuggestionsAdapter + { + + #region "Fields" + + private readonly AutoCompleteComboBox _actb; + + private string _filter; + #endregion + + #region "Constructors" + + public SuggestionsAdapter(AutoCompleteComboBox actb) + { + _actb = actb; + } + + #endregion + + #region "Methods" + + public void GetSuggestions(string searchText) + { + _actb.IsLoading = true; + // Do not open drop down if control is not focused + if (_actb.IsKeyboardFocusWithin) + _actb.IsDropDownOpen = true; + _actb.ItemsSelector.ItemsSource = null; + ParameterizedThreadStart thInfo = GetSuggestionsAsync; + Thread th = new Thread(thInfo); + _filter = searchText; + th.Start(new object[] { searchText, _actb.Provider }); + } + public void ShowFullCollection() + { + _filter = string.Empty; + _actb.IsLoading = true; + // Do not open drop down if control is not focused + if (_actb.IsKeyboardFocusWithin) + _actb.IsDropDownOpen = true; + _actb.ItemsSelector.ItemsSource = null; + ParameterizedThreadStart thInfo = GetFullCollectionAsync; + Thread th = new Thread(thInfo); + th.Start(_actb.Provider); + } + + private void DisplaySuggestions(IEnumerable suggestions, string filter) + { + if (_filter != filter) + { + return; + } + _actb.IsLoading = false; + _actb.ItemsSelector.ItemsSource = suggestions; + // Close drop down if there are no items + if (_actb.IsDropDownOpen) + { + _actb.IsDropDownOpen = _actb.ItemsSelector.HasItems; + } + } + + private void GetSuggestionsAsync(object param) + { + if (param is object[] args) + { + string searchText = Convert.ToString(args[0]); + if (args[1] is IComboSuggestionProvider provider) + { + IEnumerable list = provider.GetSuggestions(searchText); + _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, searchText); + } + } + } + private void GetFullCollectionAsync(object param) + { + if (param is IComboSuggestionProvider provider) + { + IEnumerable list = provider.GetFullCollection(); + _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, string.Empty); + } + + } + + #endregion + + } + + #endregion + + } + +} diff --git a/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs b/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs new file mode 100644 index 0000000..c2f3665 --- /dev/null +++ b/AutoCompleteTextBox/Editors/AutoCompleteTextBox.cs @@ -0,0 +1,569 @@ +using System; +using System.Collections; +using System.Threading; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Media; +using System.Windows.Threading; + +namespace AutoCompleteTextBox.Editors +{ + [TemplatePart(Name = PartEditor, Type = typeof(TextBox))] + [TemplatePart(Name = PartPopup, Type = typeof(Popup))] + [TemplatePart(Name = PartSelector, Type = typeof(Selector))] + public class AutoCompleteTextBox : Control + { + + #region "Fields" + + public const string PartEditor = "PART_Editor"; + public const string PartPopup = "PART_Popup"; + + public const string PartSelector = "PART_Selector"; + public static readonly DependencyProperty DelayProperty = DependencyProperty.Register("Delay", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(200)); + public static readonly DependencyProperty DisplayMemberProperty = DependencyProperty.Register("DisplayMember", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty)); + public static readonly DependencyProperty IconPlacementProperty = DependencyProperty.Register("IconPlacement", typeof(IconPlacement), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(IconPlacement.Left)); + public static readonly DependencyProperty IconProperty = DependencyProperty.Register("Icon", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty IconVisibilityProperty = DependencyProperty.Register("IconVisibility", typeof(Visibility), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(Visibility.Visible)); + public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty IsLoadingProperty = DependencyProperty.Register("IsLoading", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register("IsReadOnly", typeof(bool), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(false)); + public static readonly DependencyProperty ItemTemplateProperty = DependencyProperty.Register("ItemTemplate", typeof(DataTemplate), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty ItemTemplateSelectorProperty = DependencyProperty.Register("ItemTemplateSelector", typeof(DataTemplateSelector), typeof(AutoCompleteTextBox)); + public static readonly DependencyProperty LoadingContentProperty = DependencyProperty.Register("LoadingContent", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty ProviderProperty = DependencyProperty.Register("Provider", typeof(ISuggestionProvider), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null)); + public static readonly DependencyProperty SelectedItemProperty = DependencyProperty.Register("SelectedItem", typeof(object), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(null, OnSelectedItemChanged)); + public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty, propertyChangedCallback:null,coerceValueCallback:null, isAnimationProhibited:false, defaultUpdateSourceTrigger: UpdateSourceTrigger.LostFocus, flags: FrameworkPropertyMetadataOptions.BindsTwoWayByDefault)); + public static readonly DependencyProperty FilterProperty = DependencyProperty.Register("Filter", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty)); + public static readonly DependencyProperty MaxLengthProperty = DependencyProperty.Register("MaxLength", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(0)); + public static readonly DependencyProperty CharacterCasingProperty = DependencyProperty.Register("CharacterCasing", typeof(CharacterCasing), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(CharacterCasing.Normal)); + public static readonly DependencyProperty MaxPopUpHeightProperty = DependencyProperty.Register("MaxPopUpHeight", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(600)); + public static readonly DependencyProperty MaxPopUpWidthProperty = DependencyProperty.Register("MaxPopUpWidth", typeof(int), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(2000)); + + public static readonly DependencyProperty WatermarkProperty = DependencyProperty.Register("Watermark", typeof(string), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(string.Empty)); + + public static readonly DependencyProperty SuggestionBackgroundProperty = DependencyProperty.Register("SuggestionBackground", typeof(Brush), typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(Brushes.White)); + private bool _isUpdatingText; + private bool _selectionCancelled; + + private SuggestionsAdapter _suggestionsAdapter; + + + #endregion + + #region "Constructors" + + static AutoCompleteTextBox() + { + DefaultStyleKeyProperty.OverrideMetadata(typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(typeof(AutoCompleteTextBox))); + FocusableProperty.OverrideMetadata(typeof(AutoCompleteTextBox), new FrameworkPropertyMetadata(true)); + } + + #endregion + + #region "Properties" + + + public int MaxPopupHeight + { + get => (int)GetValue(MaxPopUpHeightProperty); + set => SetValue(MaxPopUpHeightProperty, value); + } + public int MaxPopupWidth + { + get => (int)GetValue(MaxPopUpWidthProperty); + set => SetValue(MaxPopUpWidthProperty, value); + } + + public BindingEvaluator BindingEvaluator { get; set; } + + public CharacterCasing CharacterCasing + { + get => (CharacterCasing)GetValue(CharacterCasingProperty); + set => SetValue(CharacterCasingProperty, value); + } + + public int MaxLength + { + get => (int)GetValue(MaxLengthProperty); + set => SetValue(MaxLengthProperty, value); + } + + public int Delay + { + get => (int)GetValue(DelayProperty); + + set => SetValue(DelayProperty, value); + } + + public string DisplayMember + { + get => (string)GetValue(DisplayMemberProperty); + + set => SetValue(DisplayMemberProperty, value); + } + + public TextBox Editor { get; set; } + + public DispatcherTimer FetchTimer { get; set; } + + public string Filter + { + get => (string)GetValue(FilterProperty); + + set => SetValue(FilterProperty, value); + } + + public object Icon + { + get => GetValue(IconProperty); + + set => SetValue(IconProperty, value); + } + + public IconPlacement IconPlacement + { + get => (IconPlacement)GetValue(IconPlacementProperty); + + set => SetValue(IconPlacementProperty, value); + } + + public Visibility IconVisibility + { + get => (Visibility)GetValue(IconVisibilityProperty); + + set => SetValue(IconVisibilityProperty, value); + } + + public bool IsDropDownOpen + { + get => (bool)GetValue(IsDropDownOpenProperty); + + set => SetValue(IsDropDownOpenProperty, value); + } + + public bool IsLoading + { + get => (bool)GetValue(IsLoadingProperty); + + set => SetValue(IsLoadingProperty, value); + } + + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + + set => SetValue(IsReadOnlyProperty, value); + } + + public Selector ItemsSelector { get; set; } + + public DataTemplate ItemTemplate + { + get => (DataTemplate)GetValue(ItemTemplateProperty); + + set => SetValue(ItemTemplateProperty, value); + } + + public DataTemplateSelector ItemTemplateSelector + { + get => ((DataTemplateSelector)(GetValue(ItemTemplateSelectorProperty))); + set => SetValue(ItemTemplateSelectorProperty, value); + } + + public object LoadingContent + { + get => GetValue(LoadingContentProperty); + + set => SetValue(LoadingContentProperty, value); + } + + public Popup Popup { get; set; } + + public ISuggestionProvider Provider + { + get => (ISuggestionProvider)GetValue(ProviderProperty); + + set => SetValue(ProviderProperty, value); + } + + public object SelectedItem + { + get => GetValue(SelectedItemProperty); + + set => SetValue(SelectedItemProperty, value); + } + + public SelectionAdapter SelectionAdapter { get; set; } + + public string Text + { + get => (string)GetValue(TextProperty); + + set => SetValue(TextProperty, value); + } + + public string Watermark + { + get => (string)GetValue(WatermarkProperty); + + set => SetValue(WatermarkProperty, value); + } + public Brush SuggestionBackground + { + get => (Brush)GetValue(SuggestionBackgroundProperty); + + set => SetValue(SuggestionBackgroundProperty, value); + } + + #endregion + + #region "Methods" + + public static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + AutoCompleteTextBox act = null; + act = d as AutoCompleteTextBox; + if (act != null) + { + if (act.Editor != null & !act._isUpdatingText) + { + act._isUpdatingText = true; + act.Editor.Text = act.BindingEvaluator.Evaluate(e.NewValue); + act._isUpdatingText = false; + } + } + } + + private void ScrollToSelectedItem() + { + if (ItemsSelector is ListBox listBox && listBox.SelectedItem != null) + listBox.ScrollIntoView(listBox.SelectedItem); + } + + public new BindingExpressionBase SetBinding(DependencyProperty dp, BindingBase binding){ + var res = base.SetBinding(dp, binding); + CheckForParentTextBindingChange(); + return res; + } + public new BindingExpressionBase SetBinding(DependencyProperty dp, String path) { + var res = base.SetBinding(dp, path); + CheckForParentTextBindingChange(); + return res; + } + public new void ClearValue(DependencyPropertyKey key) { + base.ClearValue(key); + CheckForParentTextBindingChange(); + } + public new void ClearValue(DependencyProperty dp) { + base.ClearValue(dp); + CheckForParentTextBindingChange(); + } + private void CheckForParentTextBindingChange(bool force=false) { + var CurrentBindingMode = BindingOperations.GetBinding(this, TextProperty)?.UpdateSourceTrigger ?? UpdateSourceTrigger.Default; + if (CurrentBindingMode != UpdateSourceTrigger.PropertyChanged)//preventing going any less frequent than property changed + CurrentBindingMode = UpdateSourceTrigger.Default; + + + if (CurrentBindingMode == CurrentTextboxTextBindingUpdateMode && force == false) + return; + var binding = new Binding { + Mode = BindingMode.TwoWay, + UpdateSourceTrigger = CurrentBindingMode, + Path = new PropertyPath(nameof(Text)), + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }; + CurrentTextboxTextBindingUpdateMode = CurrentBindingMode; + Editor?.SetBinding(TextBox.TextProperty, binding); + } + + private UpdateSourceTrigger CurrentTextboxTextBindingUpdateMode; + public override void OnApplyTemplate() + { + base.OnApplyTemplate(); + + Editor = Template.FindName(PartEditor, this) as TextBox; + Editor.Focus(); + Popup = Template.FindName(PartPopup, this) as Popup; + ItemsSelector = Template.FindName(PartSelector, this) as Selector; + BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember)); + + if (Editor != null) + { + Editor.TextChanged += OnEditorTextChanged; + Editor.PreviewKeyDown += OnEditorKeyDown; + Editor.LostFocus += OnEditorLostFocus; + CheckForParentTextBindingChange(true); + + if (SelectedItem != null) + { + _isUpdatingText = true; + Editor.Text = BindingEvaluator.Evaluate(SelectedItem); + _isUpdatingText = false; + } + + } + + GotFocus += AutoCompleteTextBox_GotFocus; + GotKeyboardFocus += AutoCompleteTextBox_GotKeyboardFocus; + + if (Popup != null) + { + Popup.StaysOpen = false; + Popup.Opened += OnPopupOpened; + Popup.Closed += OnPopupClosed; + } + if (ItemsSelector != null) + { + SelectionAdapter = new SelectionAdapter(ItemsSelector); + SelectionAdapter.Commit += OnSelectionAdapterCommit; + SelectionAdapter.Cancel += OnSelectionAdapterCancel; + SelectionAdapter.SelectionChanged += OnSelectionAdapterSelectionChanged; + ItemsSelector.PreviewMouseDown += ItemsSelector_PreviewMouseDown; + } + } + private void ItemsSelector_PreviewMouseDown(object sender, MouseButtonEventArgs e) + { + if ((e.OriginalSource as FrameworkElement)?.DataContext == null) + return; + if (!ItemsSelector.Items.Contains(((FrameworkElement)e.OriginalSource)?.DataContext)) + return; + ItemsSelector.SelectedItem = ((FrameworkElement)e.OriginalSource)?.DataContext; + OnSelectionAdapterCommit(SelectionAdapter.EventCause.MouseDown); + e.Handled = true; + } + private void AutoCompleteTextBox_GotFocus(object sender, RoutedEventArgs e) + { + Editor?.Focus(); + } + private void AutoCompleteTextBox_GotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e) { + if (e.NewFocus != this) + return; + if (e.OldFocus == Editor) + MoveFocus(new TraversalRequest(FocusNavigationDirection.Previous)); + + } + + private string GetDisplayText(object dataItem) + { + if (BindingEvaluator == null) + { + BindingEvaluator = new BindingEvaluator(new Binding(DisplayMember)); + } + if (dataItem == null) + { + return string.Empty; + } + if (string.IsNullOrEmpty(DisplayMember)) + { + return dataItem.ToString(); + } + return BindingEvaluator.Evaluate(dataItem); + } + + private void OnEditorKeyDown(object sender, KeyEventArgs e) + { + if (SelectionAdapter != null) + { + if (IsDropDownOpen) + SelectionAdapter.HandleKeyDown(e); + else + IsDropDownOpen = e.Key == Key.Down || e.Key == Key.Up; + } + } + + private void OnEditorLostFocus(object sender, RoutedEventArgs e) + { + if (!IsKeyboardFocusWithin) + { + IsDropDownOpen = false; + } + } + + private void OnEditorTextChanged(object sender, TextChangedEventArgs e) + { + Text = Editor.Text; + if (_isUpdatingText) + return; + if (FetchTimer == null) + { + FetchTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(Delay) }; + FetchTimer.Tick += OnFetchTimerTick; + } + FetchTimer.IsEnabled = false; + FetchTimer.Stop(); + SetSelectedItem(null); + if (Editor.Text.Length > 0) + { + FetchTimer.IsEnabled = true; + FetchTimer.Start(); + } + else + { + IsDropDownOpen = false; + } + } + + private void OnFetchTimerTick(object sender, EventArgs e) + { + FetchTimer.IsEnabled = false; + FetchTimer.Stop(); + if (Provider != null && ItemsSelector != null) + { + Filter = Editor.Text; + if (_suggestionsAdapter == null) + { + _suggestionsAdapter = new SuggestionsAdapter(this); + } + _suggestionsAdapter.GetSuggestions(Filter); + } + } + + private void OnPopupClosed(object sender, EventArgs e) + { + if (!_selectionCancelled) + { + OnSelectionAdapterCommit(SelectionAdapter.EventCause.PopupClosed); + } + } + + private void OnPopupOpened(object sender, EventArgs e) + { + _selectionCancelled = false; + ItemsSelector.SelectedItem = SelectedItem; + } + + private void OnSelectionAdapterCancel(SelectionAdapter.EventCause cause) + { + if (PreSelectionEventSomeoneHandled(cause, true)) + return; + + _isUpdatingText = true; + Editor.Text = SelectedItem == null ? Filter : GetDisplayText(SelectedItem); + Editor.SelectionStart = Editor.Text.Length; + Editor.SelectionLength = 0; + _isUpdatingText = false; + IsDropDownOpen = false; + _selectionCancelled = true; + + } + + public event EventHandler PreSelectionAdapterFinish; + private bool PreSelectionEventSomeoneHandled(SelectionAdapter.EventCause cause, bool is_cancel) { + if (PreSelectionAdapterFinish == null) + return false; + var args = new SelectionAdapter.PreSelectionAdapterFinishArgs { cause = cause, is_cancel = is_cancel }; + PreSelectionAdapterFinish?.Invoke(this, args); + return args.handled; + + } + private void OnSelectionAdapterCommit(SelectionAdapter.EventCause cause) + { + if (PreSelectionEventSomeoneHandled(cause, false)) + return; + + if (ItemsSelector.SelectedItem != null) + { + SelectedItem = ItemsSelector.SelectedItem; + _isUpdatingText = true; + Editor.Text = GetDisplayText(ItemsSelector.SelectedItem); + SetSelectedItem(ItemsSelector.SelectedItem); + _isUpdatingText = false; + IsDropDownOpen = false; + } + } + + private void OnSelectionAdapterSelectionChanged() + { + _isUpdatingText = true; + Editor.Text = ItemsSelector.SelectedItem == null ? Filter : GetDisplayText(ItemsSelector.SelectedItem); + Editor.SelectionStart = Editor.Text.Length; + Editor.SelectionLength = 0; + ScrollToSelectedItem(); + _isUpdatingText = false; + } + + private void SetSelectedItem(object item) + { + _isUpdatingText = true; + SelectedItem = item; + _isUpdatingText = false; + } + #endregion + + #region "Nested Types" + + private class SuggestionsAdapter + { + + #region "Fields" + + private readonly AutoCompleteTextBox _actb; + + private string _filter; + #endregion + + #region "Constructors" + + public SuggestionsAdapter(AutoCompleteTextBox actb) + { + _actb = actb; + } + + #endregion + + #region "Methods" + + public void GetSuggestions(string searchText) + { + _filter = searchText; + _actb.IsLoading = true; + // Do not open drop down if control is not focused + if (_actb.IsKeyboardFocusWithin) + _actb.IsDropDownOpen = true; + _actb.ItemsSelector.ItemsSource = null; + ParameterizedThreadStart thInfo = GetSuggestionsAsync; + Thread th = new Thread(thInfo); + th.Start(new object[] { searchText, _actb.Provider }); + } + + private void DisplaySuggestions(IEnumerable suggestions, string filter) + { + if (_filter != filter) + { + return; + } + _actb.IsLoading = false; + _actb.ItemsSelector.ItemsSource = suggestions; + // Close drop down if there are no items + if (_actb.IsDropDownOpen) + { + _actb.IsDropDownOpen = _actb.ItemsSelector.HasItems; + } + } + + private void GetSuggestionsAsync(object param) + { + if (param is object[] args) + { + string searchText = Convert.ToString(args[0]); + if (args[1] is ISuggestionProvider provider) + { + IEnumerable list = provider.GetSuggestions(searchText); + _actb.Dispatcher.BeginInvoke(new Action(DisplaySuggestions), DispatcherPriority.Background, list, searchText); + } + } + } + + #endregion + + } + + #endregion + + } + +} diff --git a/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs b/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs new file mode 100644 index 0000000..ca95d08 --- /dev/null +++ b/AutoCompleteTextBox/Editors/IComboSuggestionProvider.cs @@ -0,0 +1,16 @@ +using System.Collections; + + +namespace AutoCompleteTextBox.Editors +{ + public interface IComboSuggestionProvider + { + + #region Public Methods + + IEnumerable GetSuggestions(string filter); + IEnumerable GetFullCollection(); + + #endregion Public Methods + } +} diff --git a/AutoCompleteTextBox/Editors/ISuggestionProvider.cs b/AutoCompleteTextBox/Editors/ISuggestionProvider.cs new file mode 100644 index 0000000..7887021 --- /dev/null +++ b/AutoCompleteTextBox/Editors/ISuggestionProvider.cs @@ -0,0 +1,15 @@ +using System.Collections; + +namespace AutoCompleteTextBox.Editors +{ + public interface ISuggestionProvider + { + + #region Public Methods + + IEnumerable GetSuggestions(string filter); + + #endregion Public Methods + + } +} diff --git a/AutoCompleteTextBox/Editors/SelectionAdapter.cs b/AutoCompleteTextBox/Editors/SelectionAdapter.cs new file mode 100644 index 0000000..a95f0fc --- /dev/null +++ b/AutoCompleteTextBox/Editors/SelectionAdapter.cs @@ -0,0 +1,122 @@ +using System.Diagnostics; +using System.Windows.Controls.Primitives; +using System.Windows.Input; + +namespace AutoCompleteTextBox.Editors +{ + public class SelectionAdapter + { + public class PreSelectionAdapterFinishArgs { + public EventCause cause; + public bool is_cancel; + public bool handled; + } + + #region "Fields" + #endregion + + #region "Constructors" + + public SelectionAdapter(Selector selector) + { + SelectorControl = selector; + SelectorControl.PreviewMouseUp += OnSelectorMouseDown; + } + + #endregion + + #region "Events" + + public enum EventCause { Other, PopupClosed, ItemClicked, EnterPressed, EscapePressed, TabPressed, MouseDown} + public delegate void CancelEventHandler(EventCause cause); + + public delegate void CommitEventHandler(EventCause cause); + + public delegate void SelectionChangedEventHandler(); + + public event CancelEventHandler Cancel; + public event CommitEventHandler Commit; + public event SelectionChangedEventHandler SelectionChanged; + #endregion + + #region "Properties" + + public Selector SelectorControl { get; set; } + + #endregion + + #region "Methods" + + public void HandleKeyDown(KeyEventArgs key) + { + switch (key.Key) + { + case Key.Down: + IncrementSelection(); + break; + case Key.Up: + DecrementSelection(); + break; + case Key.Enter: + Commit?.Invoke(EventCause.EnterPressed); + + break; + case Key.Escape: + Cancel?.Invoke(EventCause.EscapePressed); + + break; + case Key.Tab: + Commit?.Invoke(EventCause.TabPressed); + + break; + default: + return; + } + key.Handled = true; + } + + private void DecrementSelection() + { + if (SelectorControl.SelectedIndex == -1) + { + SelectorControl.SelectedIndex = SelectorControl.Items.Count - 1; + } + else + { + SelectorControl.SelectedIndex -= 1; + } + + SelectionChanged?.Invoke(); + } + + private void IncrementSelection() + { + if (SelectorControl.SelectedIndex == SelectorControl.Items.Count - 1) + { + SelectorControl.SelectedIndex = -1; + } + else + { + SelectorControl.SelectedIndex += 1; + } + + SelectionChanged?.Invoke(); + } + + private void OnSelectorMouseDown(object sender, MouseButtonEventArgs e) + { + // If sender is the RepeatButton from the scrollbar we need to + // to skip this event otherwise focus get stuck in the RepeatButton + // and list is scrolled up or down til the end. + if (e.OriginalSource.GetType() != typeof(RepeatButton)) + { + Commit?.Invoke(EventCause.MouseDown); + e.Handled = true; + } + } + + #endregion + + } + +} \ No newline at end of file diff --git a/AutoCompleteTextBox/Editors/SuggestionProvider.cs b/AutoCompleteTextBox/Editors/SuggestionProvider.cs new file mode 100644 index 0000000..b63234f --- /dev/null +++ b/AutoCompleteTextBox/Editors/SuggestionProvider.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; + +namespace AutoCompleteTextBox.Editors +{ + public class SuggestionProvider : ISuggestionProvider + { + + + #region Private Fields + + private readonly Func _method; + + #endregion Private Fields + + #region Public Constructors + + public SuggestionProvider(Func method) + { + _method = method ?? throw new ArgumentNullException(nameof(method)); + } + + #endregion Public Constructors + + #region Public Methods + + public IEnumerable GetSuggestions(string filter) + { + return _method(filter); + } + + #endregion Public Methods + + } +} \ No newline at end of file diff --git a/AutoCompleteTextBox/Editors/Themes/Generic.xaml b/AutoCompleteTextBox/Editors/Themes/Generic.xaml new file mode 100644 index 0000000..fe4d0f3 --- /dev/null +++ b/AutoCompleteTextBox/Editors/Themes/Generic.xaml @@ -0,0 +1,268 @@ + + + + + + + + + + + + + + diff --git a/AutoCompleteTextBox/Enumerations.cs b/AutoCompleteTextBox/Enumerations.cs new file mode 100644 index 0000000..d5c0e1a --- /dev/null +++ b/AutoCompleteTextBox/Enumerations.cs @@ -0,0 +1,8 @@ +namespace AutoCompleteTextBox +{ + public enum IconPlacement + { + Left, + Right + } +} \ No newline at end of file diff --git a/AutoCompleteTextBox/Properties/AssemblyInfo.cs b/AutoCompleteTextBox/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..93c0d2c --- /dev/null +++ b/AutoCompleteTextBox/Properties/AssemblyInfo.cs @@ -0,0 +1,53 @@ +using System.Reflection; +using System.Runtime.InteropServices; +using System.Windows; +using System.Windows.Markup; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("AutoCompleteTextBox")] +[assembly: AssemblyDescription("An autocomplete textbox for WPF")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("AutoCompleteTextBox")] +[assembly: AssemblyCopyright("Copyright © 2019")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +//In order to begin building localizable applications, set +//CultureYouAreCodingWith in your .csproj file +//inside a . For example, if you are using US english +//in your source files, set the to en-US. Then uncomment +//the NeutralResourceLanguage attribute below. Update the "en-US" in +//the line below to match the UICulture setting in the project file. + +//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)] + + +[assembly:ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] + + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.1.1.0")] +[assembly: AssemblyFileVersion("1.1.1.0")] +[assembly: XmlnsDefinition("http://wpfcontrols.com/", "AutoCompleteTextBox")] +[assembly: XmlnsDefinition("http://wpfcontrols.com/", "AutoCompleteTextBox.Editors")] diff --git a/AutoCompleteTextBox/Properties/Resources.Designer.cs b/AutoCompleteTextBox/Properties/Resources.Designer.cs new file mode 100644 index 0000000..2d4da73 --- /dev/null +++ b/AutoCompleteTextBox/Properties/Resources.Designer.cs @@ -0,0 +1,63 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AutoCompleteTextBox.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("AutoCompleteTextBox.Properties.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + } +} diff --git a/AutoCompleteTextBox/Properties/Resources.resx b/AutoCompleteTextBox/Properties/Resources.resx new file mode 100644 index 0000000..af7dbeb --- /dev/null +++ b/AutoCompleteTextBox/Properties/Resources.resx @@ -0,0 +1,117 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/AutoCompleteTextBox/Properties/Settings.Designer.cs b/AutoCompleteTextBox/Properties/Settings.Designer.cs new file mode 100644 index 0000000..5bc23d8 --- /dev/null +++ b/AutoCompleteTextBox/Properties/Settings.Designer.cs @@ -0,0 +1,26 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace AutoCompleteTextBox.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.0.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); + + public static Settings Default { + get { + return defaultInstance; + } + } + } +} diff --git a/AutoCompleteTextBox/Properties/Settings.settings b/AutoCompleteTextBox/Properties/Settings.settings new file mode 100644 index 0000000..033d7a5 --- /dev/null +++ b/AutoCompleteTextBox/Properties/Settings.settings @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/AutoCompleteTextBox/Themes/Generic.xaml b/AutoCompleteTextBox/Themes/Generic.xaml new file mode 100644 index 0000000..173cb11 --- /dev/null +++ b/AutoCompleteTextBox/Themes/Generic.xaml @@ -0,0 +1,6 @@ + + + + + diff --git a/Config.Net/Stores/JsonConfigStore.cs b/Config.Net/Stores/JsonConfigStore.cs index 1859ec3..adb9326 100644 --- a/Config.Net/Stores/JsonConfigStore.cs +++ b/Config.Net/Stores/JsonConfigStore.cs @@ -43,7 +43,7 @@ namespace Config.Net.Stores // nothing to dispose. } - public string Name => "json"; + public static string Name => "json"; public bool CanRead => true; @@ -70,7 +70,7 @@ namespace Config.Net.Stores if (isIndex) { - if (!(node is JsonArray ja)) return null; + if (node is not JsonArray ja) return null; if (partIndex < ja.Count) { @@ -132,7 +132,7 @@ namespace Config.Net.Stores string js = _j.ToJsonString(new JsonSerializerOptions { WriteIndented = true }); - FileInfo file = new FileInfo(_pathName); + FileInfo file = new(_pathName); if (file is not null) { diff --git a/FSI.BT.Tools.sln b/FSI.BT.Tools.sln index 8a3d777..fb92665 100644 --- a/FSI.BT.Tools.sln +++ b/FSI.BT.Tools.sln @@ -17,6 +17,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RoboSharp", "RoboSharp\Robo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Config.Net", "Config.Net\Config.Net.csproj", "{D5C7AFF9-2226-4CC4-87F6-6303DB60FEA0}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoCompleteTextBox", "AutoCompleteTextBox\AutoCompleteTextBox.csproj", "{3162765C-B702-4927-8276-833E9046716D}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -51,6 +53,10 @@ Global {D5C7AFF9-2226-4CC4-87F6-6303DB60FEA0}.Debug|Any CPU.Build.0 = Debug|Any CPU {D5C7AFF9-2226-4CC4-87F6-6303DB60FEA0}.Release|Any CPU.ActiveCfg = Release|Any CPU {D5C7AFF9-2226-4CC4-87F6-6303DB60FEA0}.Release|Any CPU.Build.0 = Release|Any CPU + {3162765C-B702-4927-8276-833E9046716D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3162765C-B702-4927-8276-833E9046716D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3162765C-B702-4927-8276-833E9046716D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3162765C-B702-4927-8276-833E9046716D}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/FSI.BT.Tools/App.xaml.cs b/FSI.BT.Tools/App.xaml.cs index cb7e4d5..b34617d 100644 --- a/FSI.BT.Tools/App.xaml.cs +++ b/FSI.BT.Tools/App.xaml.cs @@ -1,15 +1,13 @@ -using Hardcodet.Wpf.TaskbarNotification; +using Config.Net; +using Config.Net.Stores; +using Hardcodet.Wpf.TaskbarNotification; using NHotkey; using NHotkey.Wpf; +using System.IO; +using System.IO.Compression; +using System.Reflection; using System.Windows; using System.Windows.Input; -using FSI.Lib.CompareNetObjects; -using Config.Net.Stores; -using System.IO; -using Config.Net; -using System.Collections.Generic; -using System.Linq; -using System; namespace FSI.BT.Tools { @@ -23,11 +21,14 @@ namespace FSI.BT.Tools public void Application_Startup(object sender, StartupEventArgs e) - { + { + Global.Log.Info("Anwendung wurde gestartet!"); + ExtractEmbeddedZip("FSI.BT.Tools.ExtTools.kalk.zip", Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location) + "\\ExtTools\\"); + // App-Settings - JsonConfigStore _store = new JsonConfigStore(System.IO.Path.Combine(Directory.GetCurrentDirectory(), "config.json"), true); + JsonConfigStore _store = new(System.IO.Path.Combine(Directory.GetCurrentDirectory(), "config.json"), true); Global.AppSettings = new ConfigurationBuilder() .UseConfigStore(_store) .Build(); @@ -40,9 +41,9 @@ namespace FSI.BT.Tools HotkeyManager.Current.AddOrReplace("RadialMenu", RadialMenu, ShowRadialMenu); HotkeyManager.Current.AddOrReplace("TimeStampToClipboard", TimeStamp, TimeStampToClipboard); - + Global.FrmRadialMenu = new FrmRadialMenu(); - + Global.WinCC = new Lib.Guis.SieTiaWinCCMsgMgt.ViewModel() { Data = Global.AppSettings.WinCC @@ -54,7 +55,6 @@ namespace FSI.BT.Tools Data = Global.AppSettings.IbaDirSync }; Global.Iba.Init(); - } private void ShowRadialMenu(object sender, HotkeyEventArgs e) @@ -75,13 +75,13 @@ namespace FSI.BT.Tools e.Handled = true; } - private void DeCrypt(ref IEnumerable values) + private static void ExtractEmbeddedZip(string zipName, string destPath) { - var valuesToDeCrypt = values.ToList(); - - foreach (var value in valuesToDeCrypt.ToList()) - value.ValueDeCrypt = Lib.DeEncryptString.DeEncrypt.DecryptString(value.Value, AppDomain.CurrentDomain.FriendlyName); - + System.IO.Directory.CreateDirectory(destPath); // Erstellt alle fehlenden Verzeichnisse + using Stream _pluginZipResourceStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(zipName); + using ZipArchive zip = new(_pluginZipResourceStream); + zip.ExtractToDirectory(destPath, true); + Global.Log.Info("Externes Tool \"{0}\" wurde in das Verzeichnis \"{1}\" entpackt", zipName, destPath); } private void Application_Exit(object sender, ExitEventArgs e) @@ -94,5 +94,6 @@ namespace FSI.BT.Tools } } + } } \ No newline at end of file diff --git a/FSI.BT.Tools/Commands/CmdCommand.cs b/FSI.BT.Tools/Commands/CmdCommand.cs index d7964b1..5fb03b5 100644 --- a/FSI.BT.Tools/Commands/CmdCommand.cs +++ b/FSI.BT.Tools/Commands/CmdCommand.cs @@ -26,26 +26,24 @@ namespace FSI.BT.Tools.Commands var cmds = Global.AppSettings.Cmds.ToList(); ICmd selectedCmd = null; - // IEnumerable files = new List(); - IExe selectedFile; + switch ((string)parameter) { - - case "EplPrj": - //selectedFile = GetApp(Global.AppSettings.Apps.Epl); - //Lib.Guis.Prj.Mgt.FrmMain frmMainEplPrj = new() - //{ - // ShowPdf = false, - // CloseAtLostFocus = true, - // WindowStartupLocation = WindowStartupLocation.CenterScreen, - // Path = FSI.BT.Tools.Settings.AppSettings.GetFolderByName(Global.AppSettings.Folders, "EplPrj").path, - // EplExe = selectedFile.ExePath, - //}; - //frmMainEplPrj.Show(); + + case "Epl.Prj": + Lib.Guis.Prj.Mgt.FrmMain frmMainEplPrj = new() + { + ShowPdf = false, + CloseAtLostFocus = true, + WindowStartupLocation = WindowStartupLocation.CenterScreen, + Path = FSI.BT.Tools.Settings.AppSettings.GetFolderByName(Global.AppSettings.Folders, "EplPrj").path, + EplExe = GetExeByCmdName("Epl").ExePath, + }; + frmMainEplPrj.Show(); return; - case "EplPdf": + case "Epl.Pdf": Lib.Guis.Prj.Mgt.FrmMain frmMainEplPdf = new() { ShowPdf = true, @@ -56,7 +54,7 @@ namespace FSI.BT.Tools.Commands frmMainEplPdf.Show(); return; - case "EplPdfMgt": + case "Epl.PdfMgt": Lib.Guis.Pdf.Mgt.FrmMain frmMainEplPdfMgt = new() { CloseAtLostFocus = true @@ -96,14 +94,22 @@ namespace FSI.BT.Tools.Commands }; frmTxtToClipMain.Show(); return; - + + case "Rdp.Mgt": + Lib.Guis.Rdp.Mgt.FrmMain frmRdpMain = new() + { + CloseAtLostFocus = true, + InputData = Global.AppSettings.Rdps, + Exe = GetExeByCmdName("Rdp").ExePath, + }; + frmRdpMain.Show(); + break; + default: foreach (ICmd cmd in cmds) { - if (String.Equals(parameter.ToString().ToLower(), cmd.Cmd.ToLower())) - { + if (String.Equals(parameter.ToString(), cmd.Cmd)) selectedCmd = cmd; - } } break; } @@ -121,14 +127,14 @@ namespace FSI.BT.Tools.Commands ICmd selectedCmd = null; switch ((string)parameter) - { - case "EplPrj": + { + case "Epl.Prj": return true; - case "EplPdf": + case "Epl.Pdf": return true; - case "EplPdfMgt": + case "Epl.PdfMgt": return Global.AdminRights; case "DeEncrypt": @@ -143,16 +149,16 @@ namespace FSI.BT.Tools.Commands case "TxtToClip": return Global.AppSettings.TxtToClip != null; + case "Rdp.Mgt": + return Global.AppSettings.Rdps != null; + default: foreach (ICmd cmd in cmds) { - if (String.Equals(parameter.ToString().ToLower(), cmd.Cmd.ToLower())) - { + if (String.Equals(parameter.ToString(), cmd.Cmd)) selectedCmd = cmd; - } } - break; - + break; } if (selectedCmd == null) @@ -160,26 +166,24 @@ namespace FSI.BT.Tools.Commands foreach (var file in selectedCmd.Exe.ToList()) { + if (File.Exists(Environment.ExpandEnvironmentVariables(file.ExePath.Trim()))) - { return true; - } + else if (File.Exists(Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), file.ExePath.Trim()))) + return true; } foreach (var url in selectedCmd.Urls) { if (url != String.Empty) - { return true; - } } return false; } - - private static void OpenExe(ICmd selectedCmd) - { + private static void OpenExe(ICmd selectedCmd) + { IExe selectedFile = GetApp(selectedCmd.Exe); if (selectedFile == null) @@ -196,8 +200,8 @@ namespace FSI.BT.Tools.Commands else { Process process = new(); - process.StartInfo.FileName = selectedFile.ExePath; - process.StartInfo.WorkingDirectory = selectedFile.Path == null ? selectedFile.Path : Path.GetDirectoryName(selectedFile.ExePath); + process.StartInfo.FileName = Environment.ExpandEnvironmentVariables(selectedFile.ExePath); + process.StartInfo.WorkingDirectory = selectedFile.Path ?? Path.GetDirectoryName(Environment.ExpandEnvironmentVariables(selectedFile.ExePath)); process.StartInfo.Arguments = selectedFile.Arguments; try @@ -237,7 +241,20 @@ namespace FSI.BT.Tools.Commands Thread.Sleep(100); } } - + + private static IExe GetExeByCmdName(string cmdName) + { + foreach (var cmd in Global.AppSettings.Cmds) + { + if (string.Equals(cmd.Cmd, cmdName, StringComparison.InvariantCultureIgnoreCase)) + { + return GetApp(cmd.Exe); + } + } + + return null; + } + private static bool ProgramIsRunning(string FullPath) { string FilePath = Path.GetDirectoryName(FullPath); @@ -260,7 +277,7 @@ namespace FSI.BT.Tools.Commands private static IExe GetApp(IEnumerable files) { - if(files.ToList().Count == 0) + if (files.ToList().Count == 0) return null; var selectedFile = files.ToList()[0]; diff --git a/FSI.BT.Tools/ExtTools/kalk.zip b/FSI.BT.Tools/ExtTools/kalk.zip new file mode 100644 index 0000000..bcc31d6 Binary files /dev/null and b/FSI.BT.Tools/ExtTools/kalk.zip differ diff --git a/FSI.BT.Tools/FSI.BT.Tools.csproj b/FSI.BT.Tools/FSI.BT.Tools.csproj index 774aaa7..9fc8407 100644 --- a/FSI.BT.Tools/FSI.BT.Tools.csproj +++ b/FSI.BT.Tools/FSI.BT.Tools.csproj @@ -1,22 +1,35 @@  - + net6.0-windows - WinExe + WinExe true true Icons\FondiumU.ico 2.0 + 2.0 - + - - - + + + + + + + + + + + + + + + @@ -59,10 +72,24 @@ - + + + + + + + + + + + + + + + @@ -73,7 +100,7 @@ - Never + Never @@ -109,6 +136,7 @@ + @@ -116,14 +144,19 @@ - - Always - - - Always - + + Always + + + Always + - + + + + + + diff --git a/FSI.BT.Tools/FrmRadialMenu.xaml b/FSI.BT.Tools/FrmRadialMenu.xaml index d00d865..189c215 100644 --- a/FSI.BT.Tools/FrmRadialMenu.xaml +++ b/FSI.BT.Tools/FrmRadialMenu.xaml @@ -1,14 +1,18 @@  @@ -39,6 +43,7 @@ FontSize="10" /> + @@ -207,7 +212,7 @@ + CommandParameter="Epl.Prj"> @@ -223,7 +228,7 @@ + CommandParameter="Epl.Pdf"> @@ -239,7 +244,7 @@ + CommandParameter="Epl.PdfMgt"> @@ -728,7 +733,7 @@ + CommandParameter="PL1.Pls"> @@ -744,7 +749,7 @@ + CommandParameter="PL1.Lst"> @@ -775,7 +780,7 @@ + CommandParameter="PL2.Alg"> @@ -792,7 +797,7 @@ + CommandParameter="PL2.Pls"> @@ -808,7 +813,7 @@ + CommandParameter="PL2.Als"> @@ -824,7 +829,7 @@ + CommandParameter="PL2.Lst"> @@ -840,7 +845,7 @@ + CommandParameter="PL2.Nc"> @@ -856,7 +861,7 @@ + CommandParameter="PL2.Key"> @@ -887,7 +892,7 @@ + CommandParameter="PL3.Pls"> @@ -903,7 +908,7 @@ + CommandParameter="PL3.Lst"> @@ -1109,19 +1114,75 @@ + + + + + + + + + + + + + + + + + + + + + + + +